케이스 이어하기 UI와 공통 탭·이미지 복구를 반영
This commit is contained in:
parent
72353ecd82
commit
f1b80676c1
38 changed files with 3581 additions and 455 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue