FeaturesCS Tickets (/tickets)CS Tickets (/tickets)

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(PriorityDot atom) 정합(workspace parent dir design_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 배지를 노출하고, 목록 필터에 category select(보조로 storeId URL 진입)를 추가한다(read-side LEFT 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.ts validTransitions — 클라 가드). backend 도 동일 가드를 가져 이중 방어 — 잘못된 전이는 409 TICKET_INVALID_STATUS_TRANSITION 으로 거부되고 FE 는 inline 에러 + 토스트로 처리하고 목록 새로고침을 유도한다.
  • 상태전이·담당자 배정/해제·우선순위·분류 변경은 성공 시 OperatorAuditLog 에 기록된다 (TICKET_STATUS_CHANGED·TICKET_ASSIGNED·TICKET_PRIORITY_CHANGED·TICKET_CATEGORY_CHANGED, targetType TICKET, 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). 미존재 티켓은 404 TICKET_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/ticketsuploadUrl 로 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.ts CATEGORY_META — 재생·음악/방송·안내/결제·정산/계정·로그인/기타)이며 색만으로 구분하지 않도록 라벨을 병기한다. 행은 native <tr>(암시적 role=row 유지) + tabIndex + Enter/Space 키 조작 + focus-visible ring → /tickets/{id} 상세. (role="button" 을 주면 table semantics 가 깨지므로 부여하지 않는다 — audit 테이블 관례 동일.)
  • 빈 상태total === 0 진짜 0건(“등록된 CS 티켓이 없습니다”) vs total > 0 && items 0 out-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=100 1회 조회) — 라벨 name ?? email, value id, 최상단 “미배정”(빈 값 → 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.tsxGET /api/v1/admin/tickets 를 server-side 로 fetch(보호 endpoint, OPERATOR-only — 토큰 서버 전용). audit/actions 와 동일 refresh-aware 패턴: refreshIfNeeded → fetch → 401 catch → forceRefresh 1회 재시도 → 그래도 실패 시 session.destroy() + /login. 5xx/네트워크는 errorMessage prop 으로 client banner(세션 유지). searchParams(status·priority·category·assigneeOperatorId·hqId·storeId·page)는 Next 15 Promise + 다중값 정규화. status/priority/category 는 generated enum(ListTicketsStatus/ListTicketsPriority/ ListTicketsCategory) 화이트리스트(아니면 무시). page 1-based URL → backend 0-based. searchParams 기반 key 로 client remount.
  • [id]/page.tsxGET /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 — generated useCreate(BFF catch-all /api/backend/... 경유) + 토스트. 분류는 “미분류” 기본 옵셔널 select(미선택=payload 에서 category 생략).
  • [id]/ticket-detail-client.tsx — generated useAddComment·useChangeStatus·useChangePriority· useChangeCategory·useAssign·useListOperators. 우선순위는 색점 세그먼트(생성 다이얼로그 idiom 재사용 — radiogroup/roving tabIndex, 현재값 재클릭은 no-op) → 다른 값 클릭 시 즉시 mutate. 분류는 “미분류”(null) 포함 select — 다른 값 선택 시 즉시 재분류(현재값 재선택은 no-op). 담당자는 ACTIVE 운영자 드롭다운(useListOperators status=ACTIVE·size=100) — “미배정”(빈 값=null 해제) + name ?? email 라벨. 다른 운영자 선택 시 즉시 useAssign mutate(현재 담당자 재선택은 no-op), 현재 담당자가 목록에 없으면 합성 옵션으로 유지. 운영자 조회 실패(isError)는 select 비활성 + inline 힌트. 성공 시 router.refresh(). 404/409/403/5xx 는 ticket-errors.ts mapTicketError 로 매핑(TICKET_NOT_FOUND 포함).
  • ticket-meta.ts(공용) — 상태 라벨·tone, 우선순위 라벨·색점 색/링 메타(PRIORITY_META), 카테고리 라벨·tone 메타(CATEGORY_METACATEGORY_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 만 직렬화.

사이드바 “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)