케이스 이어하기 UI와 공통 탭·이미지 복구를 반영

This commit is contained in:
Yun Chan 2026-09-01 11:45:23 +09:00
parent 72353ecd82
commit f1b80676c1
38 changed files with 3581 additions and 455 deletions

View file

@ -26,6 +26,7 @@ import {
Kicker,
ProgressBar,
surfaceClassName,
Tabs,
} from "../components/ui";
import {
adminApi,
@ -2076,25 +2077,28 @@ export default function Admin({ section = "overview" }: AdminProps) {
{usersError ? <InlineError message={usersError} /> : null}
<TabBar
<Tabs<UserTab>
id="admin-users-tabs"
ariaLabel="사용자 관리 탭"
items={[
[
"approval",
`가입 승인${accountCounts.pending ? ` ${accountCounts.pending}` : ""}`,
],
["manage", "사용자 목록"],
["register", "외부 연구참여자 사전등록"],
["activity", "활동 요약"],
{
value: "approval",
label: `가입 승인${accountCounts.pending ? ` ${accountCounts.pending}` : ""}`,
},
{ value: "manage", label: "사용자 목록" },
{ value: "register", label: "외부 연구참여자 사전등록" },
{ value: "activity", label: "활동 요약" },
]}
value={userTab}
onChange={setUserTab}
/>
{userTab === "approval" ? renderApprovalQueue() : null}
{userTab === "manage" ? renderUserList() : null}
{userTab === "register" ? renderUserCreate() : null}
{userTab === "activity" ? renderUserActivity() : null}
onValueChange={setUserTab}
listClassName="vgops-tabs"
panelClassName="vgops-tabs__panel"
>
{userTab === "approval" ? renderApprovalQueue() : null}
{userTab === "manage" ? renderUserList() : null}
{userTab === "register" ? renderUserCreate() : null}
{userTab === "activity" ? renderUserActivity() : null}
</Tabs>
</>
);
@ -2171,10 +2175,7 @@ export default function Admin({ section = "overview" }: AdminProps) {
{pendingUsers.length > 0 ? (
<div className={surfaceClassName("vgops-approval-list")}>
{pendingUsers.map((user) => (
<article
className={surfaceClassName("vgops-approval", { variant: "inset" })}
key={user.user_id}
>
<article className="vgops-approval" key={user.user_id}>
<div className="vgops-user__id">
<span aria-hidden="true">{initialOf(user)}</span>
<div>
@ -2869,91 +2870,94 @@ export default function Admin({ section = "overview" }: AdminProps) {
.
</div>
) : null}
<TabBar
<Tabs<AccessTab>
id="admin-access-tabs"
ariaLabel="접근 권한 탭"
items={[
["roles", "역할"],
["groups", "그룹"],
["matrix", "권한 매트릭스"],
["protocols", "상담 프로토콜"],
{ value: "roles", label: "역할" },
{ value: "groups", label: "그룹" },
{ value: "matrix", label: "권한 매트릭스" },
{ value: "protocols", label: "상담 프로토콜" },
]}
value={accessTab}
onChange={setAccessTab}
/>
{accessTab === "roles" ? (
<section className="vgops-policy-grid">
{ROLE_POLICIES.map((policy) => (
<article
className={surfaceClassName("vgops-policy", { variant: "inset" })}
key={policy.role}
>
<div className="vgops-section__head">
<h2>{policy.role}</h2>
<Badge tone={policy.role === "관리자" ? "warn" : "neutral"}>
{policy.permissions.length}
</Badge>
</div>
<p>{policy.scope}</p>
<div className="vgops-chip-row">
{policy.permissions.map((permission) => (
<span key={permission}>{permission}</span>
))}
</div>
<small>{policy.risk}</small>
</article>
))}
</section>
) : null}
{accessTab === "groups" ? (
<section className="vgops-policy-grid">
{GROUP_POLICIES.map((group) => (
<article
className={surfaceClassName("vgops-policy", { variant: "inset" })}
key={group.name}
>
<div className="vgops-section__head">
<h2>{group.name}</h2>
<Badge tone="neutral"> </Badge>
</div>
<p>{group.scope}</p>
<div className="vgops-chip-row">
{group.access.map((item) => (
<span key={item}>{item}</span>
))}
</div>
</article>
))}
</section>
) : null}
{accessTab === "matrix" ? (
<section className={surfaceClassName("vgops-panel")}>
<div
className={surfaceClassName("vgops-access-table", {
variant: "inset",
})}
>
<div className="vgops-access-table__head">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
{PERMISSION_MATRIX.map((row) => (
<div className="vgops-access-row" key={row.resource}>
<b>{row.resource}</b>
<span>{row.admin}</span>
<span>{row.teacher}</span>
<span>{row.learner}</span>
</div>
onValueChange={setAccessTab}
listClassName="vgops-tabs"
panelClassName="vgops-tabs__panel"
>
{accessTab === "roles" ? (
<section className="vgops-policy-grid">
{ROLE_POLICIES.map((policy) => (
<article
className={surfaceClassName("vgops-policy", { variant: "inset" })}
key={policy.role}
>
<div className="vgops-section__head">
<h2>{policy.role}</h2>
<Badge tone={policy.role === "관리자" ? "warn" : "neutral"}>
{policy.permissions.length}
</Badge>
</div>
<p>{policy.scope}</p>
<div className="vgops-chip-row">
{policy.permissions.map((permission) => (
<span key={permission}>{permission}</span>
))}
</div>
<small>{policy.risk}</small>
</article>
))}
</div>
</section>
) : null}
</section>
) : null}
{accessTab === "protocols" ? renderProtocols() : null}
{accessTab === "groups" ? (
<section className="vgops-policy-grid">
{GROUP_POLICIES.map((group) => (
<article
className={surfaceClassName("vgops-policy", { variant: "inset" })}
key={group.name}
>
<div className="vgops-section__head">
<h2>{group.name}</h2>
<Badge tone="neutral"> </Badge>
</div>
<p>{group.scope}</p>
<div className="vgops-chip-row">
{group.access.map((item) => (
<span key={item}>{item}</span>
))}
</div>
</article>
))}
</section>
) : null}
{accessTab === "matrix" ? (
<section className={surfaceClassName("vgops-panel")}>
<div
className={surfaceClassName("vgops-access-table", {
variant: "inset",
})}
>
<div className="vgops-access-table__head">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
{PERMISSION_MATRIX.map((row) => (
<div className="vgops-access-row" key={row.resource}>
<b>{row.resource}</b>
<span>{row.admin}</span>
<span>{row.teacher}</span>
<span>{row.learner}</span>
</div>
))}
</div>
</section>
) : null}
{accessTab === "protocols" ? renderProtocols() : null}
</Tabs>
</>
);
@ -3125,12 +3129,7 @@ export default function Admin({ section = "overview" }: AdminProps) {
duplicateParentId !== ticket.ticket_id &&
duplicateParentId !== ticket.parent_ticket_id;
return (
<article
className={surfaceClassName("vgops-ticket", {
variant: "inset",
})}
key={ticket.ticket_id}
>
<article className="vgops-ticket" key={ticket.ticket_id}>
<div>
{/* · <small> .
meta <small> 1 . */}
@ -3212,9 +3211,7 @@ export default function Admin({ section = "overview" }: AdminProps) {
>
{recentResolvedTickets.map((ticket) => (
<article
className={surfaceClassName("vgops-ticket vgops-ticket--history", {
variant: "inset",
})}
className="vgops-ticket vgops-ticket--history"
key={ticket.ticket_id}
>
<div>
@ -3291,39 +3288,6 @@ function EmptyState({ title, body }: { title: string; body: string }) {
);
}
function TabBar<T extends string>({
ariaLabel,
items,
value,
onChange,
}: {
ariaLabel: string;
items: Array<[T, string]>;
value: T;
onChange: (value: T) => void;
}) {
return (
<div
className={surfaceClassName("vgops-tabs", { variant: "inset", flat: true })}
role="tablist"
aria-label={ariaLabel}
>
{items.map(([id, label]) => (
<button
type="button"
key={id}
role="tab"
aria-selected={value === id}
className={value === id ? "is-active" : ""}
onClick={() => onChange(id)}
>
{label}
</button>
))}
</div>
);
}
function RoleMeter({
label,
value,