CS Tickets — /tickets (CS 티켓 v1)
SPEC #027 정합. 운영사 백오피스 CS 티켓 관리. 시각: claude design Phase 3 핸드오프 ops-tickets 시안.
Overview
고객(본사/매장)의 문의·요청을 티켓으로 추적·처리하는 백오피스 화면. 운영자(OPERATOR)가
티켓을 생성하고, 상태(OPEN → IN_PROGRESS → RESOLVED → CLOSED)를 전이시키며, 채팅형 코멘트
스레드(고객 응답 REPLY / 내부 메모 INTERNAL)로 처리 이력을 남긴다. 담당자 배정/해제와
우선순위 색점(4단계)으로 분류한다. 목록 정렬은 updatedAt desc(최근 활동 우선).
시안 출처: claude design Phase 3 핸드오프
design/screens/ops-tickets.jsx(목록·상세·생성 다이얼로그) +design/ops-phase3-shared.jsx(PriorityDotatom) 정합(workspace parent dirdesign_handoff_linkmusic/). 우선순위는 4단계 색점(PriorityDot— URGENT=danger·HIGH=warn· NORMAL=info·LOW=muted, URGENT 만 링 강조, 항상 라벨 병기)으로 표시하고, 채팅 스레드는 REPLY(고객 응대, 좌측 중립 톤) ↔ INTERNAL(내부 메모, 우측 앰버 톤 + 잠금)으로 좌우·톤을 구분한다.
채널 (운영자 백오피스 ↔ 본사·매장 self-service)
ticket 백본(테이블·TicketStatus·전이표·코멘트)은 운영자 백오피스(/tickets, 이 페이지)가
정본이며, 고객 self-service 채널은 격리 service + view DTO 로 분리된다:
- 본사 채널(
apps/space/admin/support, SPEC #086/#108) — HQ_MANAGER 가hq_id스코프로 본인 본사 티켓을 작성·조회·댓글 + 제한적 상태/우선순위 변경. HQ Customer Support. - 매장 채널(
apps/space/store/support, SPEC #112) — STORE_MANAGER 가store_id스코프로 본인 매장 티켓을 작성·조회·댓글(상태/우선순위 변경 없음 — 운영자 전담). 작성 시 category 필수. 점장 티켓은hq_id=null로 본사 화면에 안 섞인다(과노출 차단). Store Customer Support.
두 채널 모두 운영자 view 의
assignee·INTERNAL 메모 등은 view DTO 에서 제외하고(과노출 회피), 댓글은kind=REPLY만 노출한다. category 컬럼은 V35 에서 공용 ticket 테이블에 nullable 로 추가됐다. 운영자 백오피스는 SPEC #113 에서 store ticket 을 매장명(storeName)·분류(category)로 식별·트리아지 한다 — 목록·상세에 출처(매장명/본사명)·category 배지를 노출하고, 목록 필터에categoryselect(보조로storeIdURL 진입)를 추가한다(read-sideLEFT JOIN store+ DTO 필드, 마이그레이션 0). 이로써 점장 CS end-to-end 가 닫힌다(점장 작성 → 운영자가 어느 매장·무슨 분류인지 식별·응대). 본사 백오피스의 category 표면화는 여전히 후속이다.
상태머신 (TicketStatus)
| 전이 | 규칙 |
|---|---|
| 착수 | OPEN → IN_PROGRESS |
| 해결 | IN_PROGRESS → RESOLVED |
| 종결 | RESOLVED → CLOSED |
| 재오픈 | RESOLVED → IN_PROGRESS · CLOSED → IN_PROGRESS |
- UI 는 현재 status 의 유효 전이만 컨트롤로 노출한다(
ticket-meta.tsvalidTransitions— 클라 가드). backend 도 동일 가드를 가져 이중 방어 — 잘못된 전이는 409TICKET_INVALID_STATUS_TRANSITION으로 거부되고 FE 는 inline 에러 + 토스트로 처리하고 목록 새로고침을 유도한다. - 상태전이·담당자 배정/해제·우선순위·분류 변경은 성공 시
OperatorAuditLog에 기록된다 (TICKET_STATUS_CHANGED·TICKET_ASSIGNED·TICKET_PRIORITY_CHANGED·TICKET_CATEGORY_CHANGED, targetTypeTICKET, detail이전→이후/ 담당자 분기 — #047·#167)./audit/actions에서 추적한다. 티켓 타임라인의 시스템 코멘트(SYSTEM) 연동은 여전히 후속(아래 Roadmap).
우선순위 (TicketPriority)
URGENT(긴급) · HIGH(높음) · NORMAL(보통) · LOW(낮음). 목록·상세·생성 다이얼로그에서
4단계 색점(PriorityDot — 시안 ops-phase3-shared)으로 표시한다: URGENT=danger(둘레 옅은 후광
링) · HIGH=warn · NORMAL=info · LOW=muted. 색만으로 구분하지 않도록 항상 라벨을 병기한다(접근성).
목록 필터 바에는 색점 범례(legend), 생성 다이얼로그·상세 액션 사이드에는 색점 세그먼트 버튼 그룹으로
선택한다. 상세에서는 즉시 변경 가능(SPEC #046) — 현재 값과 다른 색점 클릭 시 즉시
PATCH /api/v1/admin/tickets/{id}/priority(changeTicketPriority)로 변경하고 router.refresh()
한다. 상태머신이 없어 모든 값으로 자유 전이(전이 제약·409 없음). 변경은 OperatorAuditLog 에
기록된다(TICKET_PRIORITY_CHANGED, detail 이전→이후, 상태/담당자 변경과 동일 관례 — #047).
confirm 다이얼로그 없이 즉시 실행한다(optimistic 아님). 미존재 티켓은 404 TICKET_NOT_FOUND.
분류 (TicketCategory · #167)
PLAYBACK(재생·음악) · BROADCAST(방송·안내) · BILLING(결제·정산) · ACCOUNT(계정·로그인) ·
OTHER(기타). 공용 ticket 테이블에 nullable(미분류) — 라벨은 ticket-meta.ts CATEGORY_META
(점장 support 동일 매핑)로 표시하고 색만으로 구분하지 않도록 라벨을 병기한다. 목록 필터·컬럼 배지·상세
헤더 배지에 노출한다.
운영자는 생성·재분류 양쪽에서 category 를 설정한다(#167 — 이전엔 운영자 생성 티켓이 영원히 ”—”):
- 생성 다이얼로그 — “미분류”(빈 값) 기본 + 5종
<select>(옵셔널). 미선택이면 payload 에서category를 생략(미분류). 점장 작성 폼은 category 필수지만 운영자 내부 티켓은 미분류 허용. - 상세 액션 사이드 — “미분류”(빈 값=null) 포함
<select>로 즉시 재분류. 현재 값과 다른 값 선택 시PATCH /api/v1/admin/tickets/{id}/category(changeTicketCategory)로 변경 후router.refresh(). “미분류” 선택은category=null(해제). 상태머신 없어 모든 값으로 자유 변경(전이 제약·409 없음), 동일값 재선택은 no-op. 변경은OperatorAuditLog에 기록된다(TICKET_CATEGORY_CHANGED). 미존재 티켓은 404TICKET_NOT_FOUND→ inline 에러(refresh 없음).
코멘트 스레드 (CommentKind)
시안 ChatBubble — 좌우 정렬·톤·비대칭 모서리로 종류를 구분한다.
REPLY(고객 응답) — 좌측 정렬, 중립 surface 톤(고객 응대).INTERNAL(내부 메모) — 우측 정렬, 옅은 앰버 배경 + 잠금 아이콘 + 앰버 태그(내부 통제용).
작성 폼은 kind 토글(고객 응답 / 내부 메모) + body. 빈/공백-only body 는 [코멘트 추가] 비활성.
첨부파일 (BE PR #324·#325 · 4 채널 공유)
티켓에 이미지·PDF 를 첨부한다. 운영사·본사(본인 발신)·본사(하위 매장)·점장 4 채널 모두 업로드·
다운로드가 열려 있고, FE 는 하나의 공유 컴포넌트(@linkmusic/ui TicketAttachments)를 쓰며 채널별로
mutation 과 downloadUrl 만 주입한다(4벌 복붙 금지 — frontend.md §16).
| 채널 | 화면 | 업로드 채널 인자 | 업로드 티켓 purpose |
|---|---|---|---|
| 운영사 | apps/admin /tickets/[id] | useUploadTicketAttachmentFile("ADMIN") | TICKET_ATTACHMENT_ADMIN |
| 본사(본인 발신) | apps/space /admin/support/[id] | useUploadTicketAttachmentFile("HQ") | TICKET_ATTACHMENT_HQ |
| 본사(하위 매장) | apps/space /admin/store-support/[id] | useUploadTicketAttachmentFile("HQ_STORE") | TICKET_ATTACHMENT_HQ_STORE |
| 점장 | apps/space /store/support/[id] | useUploadTicketAttachmentFile("STORE") | TICKET_ATTACHMENT_STORE |
전송 경로는 파일 크기로 갈린다(uploadTicketAttachmentFile, @linkmusic/api-client):
4MB 이하는 채널 multipart endpoint(BFF 프록시 경유, 요청 1회), 4MB 초과는 업로드 티켓
(POST /api/v1/uploads/tickets → uploadUrl 로 cross-origin multipart POST + X-Upload-Ticket)이다.
두 경로의 201 응답 계약이 같아 호출부는 파싱을 분기하지 않고, 성공 후 상세를 다시 읽어 목록을 갱신한다.
제약(BE 와 동일 — frontend.md §8): 파일당 10MB · 티켓당 5개 · MIME 5종(image/png·
image/jpeg·image/gif·image/webp·application/pdf). FE 는 크기·MIME·개수를 사전 검증해 불필요한
왕복을 막지만 판정의 최종 권한은 서버다 — BE 가 매직바이트까지 보므로 확장자만 바꾼 파일은 400 이고,
브라우저가 MIME 을 못 붙인 파일(file.type === "")은 클라가 막지 않고 서버에 맡긴다.
에러 매핑은 code 우선 + status 폴백(공유 mapTicketAttachmentError). TICKET_ATTACHMENT_* 4종 +
티켓 경로의 UPLOAD_TICKET_INVALID(401)·UPLOAD_PURPOSE_FORBIDDEN(403)·UPLOAD_NOT_CONFIGURED(503)
외에, 413·429 는 게이트웨이가 만들면 code 가 없으므로 status 로 판정한다 — 각각 “파일이 너무
큽니다” · “요청이 많습니다. 잠시 후 다시 시도해 주세요.”
(“알 수 없는 오류” 로 뭉개지 않는다).
UPLOAD_NOT_CONFIGURED 는 백엔드 UPLOAD_TICKET_SECRET 미설정 환경에서 큰 파일만 실패시키는
증상이므로 전용 문구로 원인 + “4MB 이하 파일은 지금도 올릴 수 있습니다”까지 안내한다. rate limit 한도도
경로마다 다르다 — multipart 는 계정 10회/분, 티켓 직접 업로드는 IP 20회/분.
다운로드는 같은 출처 <a download> 다. downloadUrl 은 public blob URL 이 아니라 인증이 필요한 BFF
프록시 경로이고, 같은 출처 앵커는 쿠키를 그대로 실으면서 네비게이션을 발생시키지 않는다 — 점장 티켓
상세는 player 위 모달로도 열리므로(store-player-client) window.location 대입 같은 방식은 페이지
언로드로 매장 음악을 끊는다(금지). 표시는 파일명 · 사람이 읽는 크기 · 등록 시각(KST) · 이미지/문서
아이콘 구분이며, 썸네일 미리보기는 만들지 않는다(인증 경로라 <img src> 불가 — blob 경유는 범위 밖).
진행 중 잠금(frontend.md §18) — 점장 화면은 업로드·댓글 전송·확인 종료 진행 신호를 onPendingChange
로 부모에 올리고, player 고객지원 모달이 그 동안 닫기 경로(Esc·바깥탭·X·탭 전환)를 전부 잠근다. 진행
중 닫히면 컴포넌트가 언마운트돼 mutate 콜백이 실행되지 않는데 요청은 이미 나갔으므로, 점장이 결과를 모른
채 같은 파일을 다시 올려 첨부가 중복된다. 잠금에는 상한(useModalPendingLock)이 있어 네트워크 hang
에서도 dead-end 가 되지 않는다.
가시성(운영자 채널 전용) — 운영자 응답만 TicketAttachmentAdminItem(= 공용 필드 + commentId +
visibility)이고, 비운영자 3 채널은 visibility 필드 자체가 없다(값이 항상 SHARED 일 뿐 아니라, 필드
존재가 “INTERNAL 이라는 상태가 있다”를 알려 존재 은닉을 깨기 때문). 공유 컴포넌트는 이 필드를 옵셔널로
받아 있을 때만 항목 배지를 그린다:
INTERNAL— 운영사 티켓 상세의 INTERNAL 댓글 말풍선과 같은 시각 언어(앰버--warn틴트 + 자물쇠 배지 “내부 전용”). 새 시각 언어를 만들지 않는다.SHARED— 그 항목에만 muted “공유” 배지(툴팁 “본사·점장에게도 보입니다”). 예전의 섹션 상시 경고는 제거했다 — 어느 파일이 공유 대상인지 항목별로 정확히 보이므로 전부에 대한 경고는 부정확했고, 애초에 INTERNAL 첨부를 못 만들던 우회의 잔재였다.
업로드 대상 선택(운영자 채널 전용) — 첨부 섹션의 “첨부 대상” select 로 문의 본문(공유) ↔ 내부 메모(각 INTERNAL 댓글) 를 고르면 그 commentId 로 업로드된다. 후보를 본문 + INTERNAL 댓글로 한정한
이유: REPLY 댓글에 매달아도 서버 판정이 SHARED 라 노출 범위가 본문과 같아 선택지만 늘고, 반대로 INTERNAL
댓글은 매다는 순간 운영사 전용이 되므로 반드시 고를 수 있어야 한다. 후보가 본문 하나뿐이면(=INTERNAL
댓글이 없는 티켓) select 를 아예 렌더하지 않는다. INTERNAL 판정은 서버가 댓글 kind 로 내린다(FE 는
규칙을 복제하지 않는다).
크기 전 구간(≤10MB)이 실제로 업로드된다 — 예전엔 BFF 프록시(Vercel 서버리스) 본문 상한 4.5MB 가 BE
상한 10MB 보다 낮아 그 사이 크기가 413 이었다. 이제 4MB 초과분은 프록시를 지나지 않고 백엔드로 직접
올라간다(경계 4MB = multipart 오버헤드 여유). apps/space BFF proxy 의 uploads/tickets 세션 스코프는
경로가 아니라 본문 purpose 로 갈린다 — 점장 purpose 는 실 로그인 고정이라, 임퍼소네이션 cookie 가 남은
공용 단말에서도 점장 첨부가 본사 토큰으로 새어 나가지 않는다.
UI
목록 (/tickets)
<PageHeader>제목 “CS 티켓” + 부제 “총 N건” + [티켓 생성] 버튼.- 필터 바 (server-driven, URL searchParams 단일 소스) — 상태 select · 우선순위 select ·
분류(category) select(전체 + 5종 — PLAYBACK·BROADCAST·BILLING·ACCOUNT·OTHER) · 담당자 ID 입력
- [적용]/[초기화]. 적용 시
router.push로 URL 갱신 → server component 재fetch (backend 가 필터·정렬·페이지네이션 책임). 1페이지로 리셋. (hqId·storeId는 URL 진입값을 보존 — UI 입력은 v1 생략.storeId는 점장 store ticket 트리아지용 보조 필터.)
- [적용]/[초기화]. 적용 시
- 테이블 — 우선순위 색점(
PriorityDot) · 제목 · 상태 pill · 분류 배지(category, 미설정 시 ”—”) · 출처(storeName/hqName중 채워진 쪽 — store ticket 이면 매장명, 둘 다 없으면 ”—”) · 담당자(없으면 “미배정”) · 코멘트 수 · 수정 시각(KST). category 는 점장 support 와 동일 라벨 매핑(ticket-meta.tsCATEGORY_META— 재생·음악/방송·안내/결제·정산/계정·로그인/기타)이며 색만으로 구분하지 않도록 라벨을 병기한다. 행은 native<tr>(암시적role=row유지) +tabIndex+ Enter/Space 키 조작 +focus-visiblering →/tickets/{id}상세. (role="button"을 주면 table semantics 가 깨지므로 부여하지 않는다 — audit 테이블 관례 동일.) - 빈 상태 —
total === 0진짜 0건(“등록된 CS 티켓이 없습니다”) vstotal > 0 && items 0out-of-range page(“이 페이지에 표시할 티켓이 없습니다” + [첫 페이지로]) 별도. page clamp. - 에러 상태 —
errorMessage(5xx/네트워크)면 본문 대신 공용ErrorState([다시 시도]=router.refresh(),tickets-error). 빈 상태(0건)와 구분 — 재시도 CTA 로 dead-end 해소(SPEC #154). - 생성 다이얼로그 — title·body·priority(필수, 색점 세그먼트 버튼 그룹) + 분류 select(옵셔널, 미분류
기본) + hqId·storeId(선택). 필수 필드 사전 검증(zod trim·min) — 미통과 시 [생성] 비활성. 성공 시
router.refresh()+ 토스트.
상세 (/tickets/{id})
- 헤더 — title · 우선순위 색점(
PriorityDot) · 상태 pill · 분류 배지(category, null 이면 생략) · 출처(store ticket 이면매장: {storeName}, 아니면본사: {hqName}) · 담당자 · 생성/수정 시각(KST)- [목록] 링크.
- 본문 + 채팅형 코멘트 스레드 — REPLY/INTERNAL 시각 구분. 코멘트 0건이면 “아직 코멘트가 없습니다”.
- 첨부파일 섹션(본문 바로 아래) — 공유
TicketAttachments(surface="ops"). [파일 첨부] + 목록(파일명· 크기·등록 시각·내려받기). 0건이면 “첨부된 파일이 없습니다.”, 5개면 컨트롤 잠금. 업로드 성공 시router.refresh(). 운영사 전용 경고 문구로 노출 범위를 명시한다. - 코멘트 작성 폼 — kind 토글 + body + [코멘트 추가].
- 액션 사이드 — 상태 변경(유효 전이만 버튼 노출) · 우선순위 변경(4단계 색점 세그먼트 — 다른 값
클릭 시 즉시 변경, 자유 전이) · 분류 변경(select — “미분류”(null) 포함, 다른 값 선택 시 즉시
재분류) · 담당자 배정(ACTIVE 운영자 드롭다운 — SPEC #173 D6, raw UUID 입력 대체). 소스는
generated
useListOperators(status=ACTIVE서버 필터 + 클라 재필터,size=1001회 조회) — 라벨name ?? email, valueid, 최상단 “미배정”(빈 값 →assigneeOperatorId미포함 = null 해제). 다른 운영자 선택 시 즉시 mutate(useAssign), 현재 담당자 재선택은 no-op. 현재 담당자가 ACTIVE 목록에 없으면(정지·회수 등) 선택 표시 유실을 막으려assigneeEmail라벨의 합성 옵션으로 최상단에 유지한다. 조회 중(isPending)·조회 실패(isError)는 select 비활성 + 인라인 힌트로 구분한다. - detail 이 null 이면(5xx/네트워크) 에러 배너. 404
TICKET_NOT_FOUND는 page 에서notFound().
시각 표시 (KST)
createdAt/updatedAt/코멘트 createdAt(backend UTC)은
Intl.DateTimeFormat("sv-SE", { timeZone: "Asia/Seoul", ... })(ticket-meta.ts formatKstDateTime)로
KST clock time 표시. timeZone 명시로 SSR/CSR 가 머신 TZ 와 무관하게 동일 결과(문자열 slice 미사용).
Implementation
Page (server component, refresh-aware)
apps/admin/src/app/(protected)/tickets/page.tsx—GET /api/v1/admin/tickets를 server-side 로 fetch(보호 endpoint, OPERATOR-only — 토큰 서버 전용). audit/actions 와 동일 refresh-aware 패턴:refreshIfNeeded→ fetch → 401 catch →forceRefresh1회 재시도 → 그래도 실패 시session.destroy()+/login. 5xx/네트워크는errorMessageprop 으로 client banner(세션 유지). searchParams(status·priority·category·assigneeOperatorId·hqId·storeId·page)는 Next 15 Promise + 다중값 정규화.status/priority/category는 generated enum(ListTicketsStatus/ListTicketsPriority/ListTicketsCategory) 화이트리스트(아니면 무시).page1-based URL → backend 0-based. searchParams 기반key로 client remount.[id]/page.tsx—GET /api/v1/admin/tickets/{id}server-side fetch./hq/[id]의 notFound 관용구(404 →notFound()).
Client
tickets-list-client.tsx— 필터 hydrate(enum 화이트리스트)·URL push·페이지네이션. 행 클릭/키보드 → 상세 네비.<TicketCreateDialog>마운트.ticket-create-dialog.tsx— generateduseCreate(BFF catch-all/api/backend/...경유) + 토스트. 분류는 “미분류” 기본 옵셔널 select(미선택=payload 에서 category 생략).[id]/ticket-detail-client.tsx— generateduseAddComment·useChangeStatus·useChangePriority·useChangeCategory·useAssign·useListOperators. 우선순위는 색점 세그먼트(생성 다이얼로그 idiom 재사용 — radiogroup/roving tabIndex, 현재값 재클릭은 no-op) → 다른 값 클릭 시 즉시 mutate. 분류는 “미분류”(null) 포함 select — 다른 값 선택 시 즉시 재분류(현재값 재선택은 no-op). 담당자는 ACTIVE 운영자 드롭다운(useListOperatorsstatus=ACTIVE·size=100) — “미배정”(빈 값=null 해제) +name ?? email라벨. 다른 운영자 선택 시 즉시useAssignmutate(현재 담당자 재선택은 no-op), 현재 담당자가 목록에 없으면 합성 옵션으로 유지. 운영자 조회 실패(isError)는 select 비활성 + inline 힌트. 성공 시router.refresh(). 404/409/403/5xx 는ticket-errors.tsmapTicketError로 매핑(TICKET_NOT_FOUND포함).ticket-meta.ts(공용) — 상태 라벨·tone, 우선순위 라벨·색점 색/링 메타(PRIORITY_META), 카테고리 라벨·tone 메타(CATEGORY_META)·CATEGORY_ORDER(점장 support 동일 라벨 미러),validTransitions,formatKstDateTime. nullable category 는TicketCategory = NonNullable<...>로 좁혀 null 은 ”—” 폴백.priority-dot.tsx(공용) — 우선순위 4단계 색점 컴포넌트(PriorityDot, 시안ops-phase3-shared).
응답/요청 타입은 generated 스키마(@linkmusic/api-client) 단일 소스 — 수기 중복 정의 없음. server
helper backendListTickets/backendGetTicketDetail(lib/backend.ts)는 정의된 param 만 직렬화.
Navigation
사이드바 “CS 티켓”(/tickets)은 기존 NAV_ITEMS 에 이미 존재(SPEC 이전 placeholder href) — 변경 없음.
States & Edge Cases
| 상태 | 처리 |
|---|---|
| 티켓 0건 | ”등록된 CS 티켓이 없습니다” 빈 상태 (total === 0) |
| out-of-range page | ”이 페이지에 표시할 티켓이 없습니다” + [첫 페이지로] (total > 0, items 0) |
| 잘못된 상태 전이 | 409 TICKET_INVALID_STATUS_TRANSITION → inline 에러 + 새로고침 유도 (클라 가드로 1차 차단) |
| 5xx / 네트워크 실패 | client banner (세션 유지, 강제 로그아웃 X) |
| 401 (refresh 실패) | session.destroy() + /login redirect |
| 404 (미존재 id) | notFound() |
| 잘못된 status/priority/category query | 무시(필터 미적용 — 화이트리스트) |
| 담당자 미배정 | 목록 “미배정” · 상세 드롭다운 “미배정” 옵션 선택(null 해제) |
| 운영자 목록 조회 실패 | 상세 담당자 select 비활성 + inline 힌트(“운영자 목록을 불러오지 못했습니다”) — “운영자 없음” 오인 방지 |
| category null (미분류) | 목록·상세 분류 배지 대신 ”—” / 생략. 운영자는 생성·상세 select 로 분류·재분류 가능(#167) |
| storeName·hqName 둘 다 null (legacy) | 출처 ”—“ |
사이드바 CS 카운트 배지 (SPEC #119 F1 · BE 0)
운영사 OpsSidebar 의 /tickets(CS 티켓) 항목 우측에 미해결 카운트 pill 을 표시한다.
기존 GET /api/v1/admin/stats(AdminStatsResponse.tickets)를 재사용해 open + inProgress 를
바인딩한다 — 신규 BE 0. 대시보드(DashboardStats)의 useGetAdminStats() 와 동일 queryKey 이므로
react-query 가 dedup(중복 호출 0). 사이드바는 60초 폴링 옵션만 부여
(refetchInterval:60s·refetchIntervalInBackground:false — 탭 비활성 중단, focus 복귀 시 즉시).
SidebarItem.badge?: number prop(apps/admin/src/components/shell/sidebar.tsx)으로 추가했고, count 0·
로딩·에러 시 미표시(sidebar-badge-/tickets, 99 초과는 99+). pill 시각은 atom-grounded(sidebar-star
도트 패턴과 동형 — primary 톤·tabular-nums, 전용 시안 부재로 design-debt 등재).
Roadmap (후속)
첨부파일(BE PR #324 로 4 채널 완료 — 위 첨부파일) · SLA/마감 · 고객 알림(이메일/푸시) · 태그.- 본사 백오피스의 category 표면화(운영자 view 는 #113 에서 완료).
- 목록 필터의 담당자 검색 picker(현재 목록 필터 바는 여전히 “담당자 ID 입력” raw — 상세의 담당자
컨트롤은 SPEC #173 D6 에서 이미 ACTIVE 운영자 드롭다운으로 완료) · hqId/storeId 검색 picker(현재
storeId는 URL 진입만). - CS stats 대시보드 연동(대기 티켓 수).
- 시스템 코멘트(SYSTEM) 연동 — 상태/담당자/우선순위 변경을 티켓 타임라인에 SYSTEM 코멘트로도
남기기(현재는
OperatorAuditLog기록만 — #047). 이전 담당자 추적(현재 assign detail 은 결과만).
References
- SPEC #027 · #046 (우선순위 변경 컨트롤) · #047 (감사 로그 연동 — 상태/담당자/우선순위 변경
OperatorAuditLog기록) · #031-A (시안 교체) · #026 (audit 테이블 관용구 재사용) · #018/#024 (Dialog 전이 관용구) · #112 (점장 매장 채널 + category) · #113 (운영자 store ticket 식별 — storeName·category 표면화·필터, 점장 CS end-to-end 마감) · #173 D6 (상세 담당자 배정 = ACTIVE 운영자 드롭다운, raw UUID 입력 대체 — FE-only) linkmusic-frontend-space/packages/ui/src/components/ticket-attachments.tsx(4 채널 공유 첨부 본체 +mapTicketAttachmentError·validateTicketAttachmentFile·ticketAttachmentHref)linkmusic-frontend-space/apps/admin/src/app/(protected)/tickets/(page.tsx·tickets-list-client.tsx·ticket-create-dialog.tsx·ticket-meta.ts·priority-dot.tsx·ticket-errors.ts·[id]/page.tsx·[id]/ticket-detail-client.tsx)- 시안: workspace parent dir
design_handoff_linkmusic/design/screens/ops-tickets.jsx·design/ops-phase3-shared.jsx(PriorityDot)