diff --git a/docs/ycshin-node/CCA[계획]-카테고리-연속등록모드.md b/docs/ycshin-node/CCA[계획]-카테고리-연속등록모드.md
new file mode 100644
index 00000000..964c389f
--- /dev/null
+++ b/docs/ycshin-node/CCA[계획]-카테고리-연속등록모드.md
@@ -0,0 +1,199 @@
+# [계획서] 카테고리 트리 대분류 추가 모달 - 연속 등록 모드 수정
+
+> 관련 문서: [맥락노트](./CCA[맥락]-카테고리-연속등록모드.md) | [체크리스트](./CCA[체크]-카테고리-연속등록모드.md)
+
+## 개요
+
+기준정보 - 옵션설정 화면에서 트리 구조 카테고리(예: 품목정보 > 재고단위)의 "대분류 추가" 모달이 저장 후 닫히지 않는 버그를 수정합니다.
+평면 목록용 추가 모달(`CategoryValueAddDialog.tsx`)과 동일한 연속 입력 패턴을 적용합니다.
+
+---
+
+## 현재 동작
+
+- 대분류 추가 모달에서 값 입력 후 "추가" 클릭 시 **값은 정상 저장됨**
+- 저장 후 **모달이 닫히지 않고** 폼만 초기화됨 (항상 연속 입력 상태)
+- "연속 입력" 체크박스 UI가 **없음** → 사용자가 모드를 끌 수 없음
+- 모달을 닫으려면 "닫기" 버튼 또는 외부 클릭을 해야 함
+
+### 현재 코드 (CategoryValueManagerTree.tsx - handleAdd, 512~530행)
+
+```tsx
+if (response.success) {
+ toast.success("카테고리가 추가되었습니다");
+ // 폼 초기화 (모달은 닫지 않고 연속 입력)
+ setFormData((prev) => ({
+ ...prev,
+ valueCode: "",
+ valueLabel: "",
+ description: "",
+ color: "",
+ }));
+ setTimeout(() => addNameRef.current?.focus(), 50);
+ await loadTree(true);
+ if (parentValue) {
+ setExpandedNodes((prev) => new Set([...prev, parentValue.valueId]));
+ }
+}
+```
+
+### 현재 DialogFooter (809~821행)
+
+```tsx
+
+
+
+
+```
+
+---
+
+## 변경 후 동작
+
+### 1. 기본 동작: 저장 후 모달 닫힘
+
+- "추가" 클릭 → 저장 성공 → 모달 닫힘 + 트리 새로고침
+- `CategoryValueAddDialog.tsx`(평면 목록 추가 모달)와 동일한 기본 동작
+
+### 2. 연속 입력 체크박스 추가
+
+- DialogFooter 좌측에 "연속 입력" 체크박스 표시
+- 기본값: 체크 해제 (OFF)
+- 체크 시: 저장 후 폼만 초기화, 모달 유지, 이름 필드에 포커스
+- 체크 해제 시: 저장 후 모달 닫힘
+
+---
+
+## 시각적 예시
+
+| 상태 | 연속 입력 체크 | 추가 버튼 클릭 후 |
+|------|---------------|-----------------|
+| 기본 (체크 해제) | [ ] 연속 입력 | 저장 → 모달 닫힘 → 트리 갱신 |
+| 연속 모드 (체크) | [x] 연속 입력 | 저장 → 폼 초기화 → 모달 유지 → 이름 필드 포커스 |
+
+### 모달 하단 레이아웃 (ScreenModal.tsx 패턴)
+
+```
+┌─────────────────────────────────────────┐
+│ [닫기] [추가] │ ← DialogFooter (버튼만)
+├─────────────────────────────────────────┤
+│ [x] 저장 후 계속 입력 (연속 등록 모드) │ ← border-t 구분선 아래 별도 영역
+└─────────────────────────────────────────┘
+```
+
+---
+
+## 아키텍처
+
+```mermaid
+flowchart TD
+ A["사용자: '추가' 클릭"] --> B["handleAdd()"]
+ B --> C{"API 호출 성공?"}
+ C -- 실패 --> D["toast.error → 모달 유지"]
+ C -- 성공 --> E["toast.success + loadTree"]
+ E --> F{"continuousAdd?"}
+ F -- true --> G["폼 초기화 + 이름 필드 포커스\n모달 유지"]
+ F -- false --> H["폼 초기화 + 모달 닫힘"]
+```
+
+---
+
+## 변경 대상 파일
+
+| 파일 | 역할 | 변경 내용 |
+|------|------|----------|
+| `frontend/components/table-category/CategoryValueManagerTree.tsx` | 트리형 카테고리 값 관리 | 상태 추가, handleAdd 분기, DialogFooter UI |
+
+- **변경 규모**: 약 20줄 내외 소규모 변경
+- **참고 파일**: `frontend/components/table-category/CategoryValueAddDialog.tsx` (동일 패턴)
+
+---
+
+## 코드 설계
+
+### 1. 상태 추가 (286행 근처, 모달 상태 선언부)
+
+```tsx
+const [continuousAdd, setContinuousAdd] = useState(false);
+```
+
+### 2. handleAdd 성공 분기 수정 (512~530행 대체)
+
+```tsx
+if (response.success) {
+ toast.success("카테고리가 추가되었습니다");
+ await loadTree(true);
+ if (parentValue) {
+ setExpandedNodes((prev) => new Set([...prev, parentValue.valueId]));
+ }
+
+ if (continuousAdd) {
+ setFormData((prev) => ({
+ ...prev,
+ valueCode: "",
+ valueLabel: "",
+ description: "",
+ color: "",
+ }));
+ setTimeout(() => addNameRef.current?.focus(), 50);
+ } else {
+ setFormData({ valueCode: "", valueLabel: "", description: "", color: "", isActive: true });
+ setIsAddModalOpen(false);
+ }
+}
+```
+
+### 3. DialogFooter + 연속 등록 체크박스 수정 (809~821행 대체)
+
+DialogFooter는 버튼만 유지하고, 그 아래에 `border-t` 구분선과 체크박스를 별도 영역으로 배치합니다.
+`ScreenModal.tsx` (1287~1303행) 패턴 그대로입니다.
+
+```tsx
+
+
+
+
+
+{/* 연속 등록 모드 체크박스 - ScreenModal.tsx 패턴 */}
+
+
+ setContinuousAdd(checked as boolean)}
+ />
+
+
+
+```
+
+---
+
+## 예상 문제 및 대응
+
+`CategoryValueAddDialog.tsx`와 동일한 패턴이므로 별도 예상 문제 없음.
+
+---
+
+## 설계 원칙
+
+- `CategoryValueAddDialog.tsx`(같은 폴더, 같은 목적)의 패턴을 그대로 따름
+- 기존 수정/삭제 모달 동작은 변경하지 않음
+- 하위 추가(중분류/소분류) 모달도 동일한 `handleAdd`를 사용하므로 자동 적용
+- `Checkbox` import는 이미 존재 (24행)하므로 추가 import 불필요
+- `Label` import는 이미 존재 (53행)하므로 추가 import 불필요
+- 체크박스 위치/라벨/className 모두 `ScreenModal.tsx` (1287~1303행)과 동일
diff --git a/docs/ycshin-node/CCA[맥락]-카테고리-연속등록모드.md b/docs/ycshin-node/CCA[맥락]-카테고리-연속등록모드.md
new file mode 100644
index 00000000..1b5cb92e
--- /dev/null
+++ b/docs/ycshin-node/CCA[맥락]-카테고리-연속등록모드.md
@@ -0,0 +1,84 @@
+# [맥락노트] 카테고리 트리 대분류 추가 모달 - 연속 등록 모드 수정
+
+> 관련 문서: [계획서](./CCA[계획]-카테고리-연속등록모드.md) | [체크리스트](./CCA[체크]-카테고리-연속등록모드.md)
+
+---
+
+## 왜 이 작업을 하는가
+
+- 기준정보 - 옵션설정에서 트리 구조 카테고리(품목정보 > 재고단위 등)의 "대분류 추가" 모달이 저장 후 닫히지 않음
+- 연속 등록 모드가 하드코딩되어 항상 ON 상태이고, 끌 수 있는 UI가 없음
+- 같은 폴더의 평면 목록 모달(`CategoryValueAddDialog.tsx`)은 이미 올바르게 구현되어 있음
+- 동일 패턴을 적용하여 일관성 확보
+
+---
+
+## 핵심 결정 사항과 근거
+
+### 1. 기본값: 연속 등록 OFF (모달 닫힘)
+
+- **결정**: `continuousAdd` 초기값을 `false`로 설정
+- **근거**: 대부분의 사용자는 한 건 추가 후 결과를 확인하려 함. 연속 입력은 선택적 기능
+
+### 2. 체크박스 위치: DialogFooter 아래, border-t 구분선 별도 영역
+
+- **결정**: `ScreenModal.tsx` (1287~1303행) 패턴 그대로 적용
+- **근거**: "기준정보 - 부서관리" 추가 모달과 동일한 디자인. 프로젝트 관행 준수
+- **대안 검토**: `CategoryValueAddDialog.tsx`는 DialogFooter 안에 체크박스 배치 → 부서 모달과 다른 디자인이므로 기각
+
+### 3. 라벨: "저장 후 계속 입력 (연속 등록 모드)"
+
+- **결정**: `ScreenModal.tsx`와 동일한 라벨 텍스트 사용
+- **근거**: 부서 추가 모달과 동일한 문구로 사용자 혼란 방지
+
+### 4. localStorage 미사용
+
+- **결정**: 컴포넌트 state만 사용, localStorage 영속화 안 함
+- **근거**: `CategoryValueAddDialog.tsx`(같은 폴더 형제 컴포넌트)가 localStorage를 쓰지 않음. `ScreenModal.tsx`는 사용하지만 동적 화면 모달 전용 기능이므로 범위가 다름
+
+### 5. 수정 대상: handleAdd 함수만
+
+- **결정**: 저장 성공 분기에서만 `continuousAdd` 체크
+- **근거**: 실패 시에는 원래대로 모달 유지 + 에러 표시. 분기가 필요한 건 성공 시뿐
+
+---
+
+## 관련 파일 위치
+
+| 구분 | 파일 경로 | 설명 |
+|------|----------|------|
+| 수정 대상 | `frontend/components/table-category/CategoryValueManagerTree.tsx` | 트리형 카테고리 값 관리 (대분류/중분류/소분류) |
+| 참고 패턴 (로직) | `frontend/components/table-category/CategoryValueAddDialog.tsx` | 평면 목록 추가 모달 - continuousAdd 분기 로직 |
+| 참고 패턴 (UI) | `frontend/components/common/ScreenModal.tsx` | 동적 화면 모달 - 체크박스 위치/라벨/스타일 |
+
+---
+
+## 기술 참고
+
+### 현재 handleAdd 흐름
+
+```
+handleAdd() → API 호출 → 성공 시:
+ 1. toast.success
+ 2. 폼 초기화 (모달 유지 - 하드코딩)
+ 3. addNameRef 포커스
+ 4. loadTree(true) - 펼침 상태 유지
+ 5. parentValue 있으면 해당 노드 펼침
+```
+
+### 변경 후 handleAdd 흐름
+
+```
+handleAdd() → API 호출 → 성공 시:
+ 1. toast.success
+ 2. loadTree(true) + parentValue 펼침
+ 3. continuousAdd 체크:
+ - true: 폼 초기화 + addNameRef 포커스 (모달 유지)
+ - false: 폼 초기화 + setIsAddModalOpen(false) (모달 닫힘)
+```
+
+### import 현황
+
+- `Checkbox`: 24행에서 이미 import (`@/components/ui/checkbox`)
+- `Label`: 53행에서 이미 import (`@/components/ui/label`)
+- 추가 import 불필요
diff --git a/docs/ycshin-node/CCA[체크]-카테고리-연속등록모드.md b/docs/ycshin-node/CCA[체크]-카테고리-연속등록모드.md
new file mode 100644
index 00000000..f794e0ff
--- /dev/null
+++ b/docs/ycshin-node/CCA[체크]-카테고리-연속등록모드.md
@@ -0,0 +1,52 @@
+# [체크리스트] 카테고리 트리 대분류 추가 모달 - 연속 등록 모드 수정
+
+> 관련 문서: [계획서](./CCA[계획]-카테고리-연속등록모드.md) | [맥락노트](./CCA[맥락]-카테고리-연속등록모드.md)
+
+---
+
+## 공정 상태
+
+- 전체 진행률: **100%** (구현 완료)
+- 현재 단계: 완료
+
+---
+
+## 구현 체크리스트
+
+### 1단계: 상태 추가
+
+- [x] `CategoryValueManagerTree.tsx` 모달 상태 선언부(286행 근처)에 `continuousAdd` 상태 추가
+
+### 2단계: handleAdd 분기 수정
+
+- [x] `handleAdd` 성공 분기(512~530행)에서 `continuousAdd` 체크 분기 추가
+- [x] `continuousAdd === true`: 폼 초기화 + addNameRef 포커스 (모달 유지)
+- [x] `continuousAdd === false`: 폼 초기화 + `setIsAddModalOpen(false)` (모달 닫힘)
+
+### 3단계: DialogFooter UI 수정
+
+- [x] DialogFooter(809~821행)는 버튼만 유지
+- [x] DialogFooter 아래에 `border-t px-4 py-3` 영역 추가
+- [x] "저장 후 계속 입력 (연속 등록 모드)" 체크박스 배치
+- [x] ScreenModal.tsx (1287~1303행) 패턴과 동일한 className/라벨 사용
+
+### 4단계: 검증
+
+- [ ] 대분류 추가: 체크 해제 상태에서 추가 → 모달 닫힘 확인
+- [ ] 대분류 추가: 체크 상태에서 추가 → 모달 유지 + 폼 초기화 + 포커스 확인
+- [ ] 하위 추가(중분류/소분류): 동일하게 동작하는지 확인
+- [ ] 수정/삭제 모달: 기존 동작 변화 없음 확인
+
+### 5단계: 정리
+
+- [x] 린트 에러 없음 확인
+- [x] 이 체크리스트 완료 표시 업데이트
+
+---
+
+## 변경 이력
+
+| 날짜 | 내용 |
+|------|------|
+| 2026-03-11 | 계획서, 맥락노트, 체크리스트 작성 완료 |
+| 2026-03-11 | 구현 완료 (1~3단계, 5단계 정리). 4단계 검증은 수동 테스트 필요 |
diff --git a/docs/ycshin-node/CTI[계획]-카테고리-깊이구분.md b/docs/ycshin-node/CTI[계획]-카테고리-깊이구분.md
new file mode 100644
index 00000000..7b524b82
--- /dev/null
+++ b/docs/ycshin-node/CTI[계획]-카테고리-깊이구분.md
@@ -0,0 +1,122 @@
+# [계획서] 카테고리 드롭다운 - 3단계 깊이 구분 표시
+
+> 관련 문서: [맥락노트](./CTI[맥락]-카테고리-깊이구분.md) | [체크리스트](./CTI[체크]-카테고리-깊이구분.md)
+>
+> 상태: **완료** (2026-03-11)
+
+## 개요
+
+카테고리 타입(`source="category"`) 드롭다운에서 3단계 계층(대분류 > 중분류 > 소분류)의 들여쓰기가 시각적으로 구분되지 않는 문제를 수정합니다.
+
+---
+
+## 변경 전 동작
+
+- `category_values` 테이블은 `parent_value_id`, `depth` 컬럼으로 3단계 계층 구조를 지원
+- 백엔드 `buildHierarchy()`가 트리 구조를 정상적으로 반환
+- 프론트엔드 `flattenTree()`가 트리를 평탄화하면서 **일반 ASCII 공백(`" "`)** 으로 들여쓰기 생성
+- HTML이 연속 공백을 하나로 축소(collapse)하여 depth 1과 depth 2가 동일하게 렌더링됨
+
+### 변경 전 코드 (flattenTree)
+
+```tsx
+const prefix = depth > 0 ? " ".repeat(depth) + "└ " : "";
+```
+
+### 변경 전 렌더링 결과
+
+```
+신예철
+└ 신2
+└ 신22 ← depth 2인데 depth 1과 구분 불가
+└ 신3
+└ 신4
+```
+
+---
+
+## 변경 후 동작
+
+### 일반 공백을 Non-Breaking Space(`\u00A0`)로 교체
+
+- `\u00A0`는 HTML에서 축소되지 않으므로 depth별 들여쓰기가 정확히 유지됨
+- depth당 3칸(`\u00A0\u00A0\u00A0`)으로 시각적 계층 구분을 명확히 함
+- 백엔드 변경 없음 (트리 구조는 이미 정상)
+
+### 변경 후 코드 (flattenTree)
+
+```tsx
+const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
+```
+
+---
+
+## 시각적 예시
+
+| depth | prefix | 드롭다운 표시 |
+|-------|--------|-------------|
+| 0 (대분류) | `""` | `신예철` |
+| 1 (중분류) | `"\u00A0\u00A0\u00A0└ "` | `···└ 신2` |
+| 2 (소분류) | `"\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0└ "` | `······└ 신22` |
+
+### 변경 전후 비교
+
+```
+변경 전: 변경 후:
+신예철 신예철
+└ 신2 └ 신2
+└ 신22 ← 구분 불가 └ 신22 ← 명확히 구분
+└ 신3 └ 신3
+└ 신4 └ 신4
+```
+
+---
+
+## 아키텍처
+
+```mermaid
+flowchart TD
+ A[category_values 테이블] -->|parent_value_id, depth| B[백엔드 buildHierarchy]
+ B -->|트리 JSON 응답| C[프론트엔드 API 호출]
+ C --> D[flattenTree 함수]
+ D -->|"depth별 \u00A0 prefix 생성"| E[SelectOption 배열]
+ E --> F{렌더링 모드}
+ F -->|비검색| G[SelectItem - label 표시]
+ F -->|검색| H[CommandItem - displayLabel 표시]
+
+ style D fill:#f96,stroke:#333,color:#000
+```
+
+**변경 지점**: `flattenTree` 함수 내 prefix 생성 로직 (주황색 표시)
+
+---
+
+## 변경 대상 파일
+
+| 파일 경로 | 변경 내용 | 변경 규모 |
+|-----------|----------|----------|
+| `frontend/components/v2/V2Select.tsx` (904행) | `flattenTree` prefix를 `\u00A0` 기반으로 변경 | 1줄 |
+| `frontend/components/unified/UnifiedSelect.tsx` (632행) | 동일한 `flattenTree` prefix 변경 | 1줄 |
+
+---
+
+## 영향받는 기존 로직
+
+V2Select.tsx의 `resolvedValue`(979행)에서 prefix를 제거하는 정규식:
+
+```tsx
+const cleanLabel = o.label.replace(/^[\s└]+/, "").trim();
+```
+
+- JavaScript `\s`는 `\u00A0`를 포함하므로 기존 정규식이 정상 동작함
+- 추가 수정 불필요
+
+---
+
+## 설계 원칙
+
+- 백엔드 변경 없이 프론트엔드 표시 로직만 수정
+- `flattenTree` 공통 함수 수정이므로 카테고리 타입 드롭다운 전체에 자동 적용
+- DB 저장값(`valueCode`)에는 영향 없음 — `label`만 변경
+- 기존 prefix strip 정규식(`/^[\s└]+/`)과 호환 유지
+- `V2Select`와 `UnifiedSelect` 두 곳의 동일 패턴을 일관되게 수정
diff --git a/docs/ycshin-node/CTI[맥락]-카테고리-깊이구분.md b/docs/ycshin-node/CTI[맥락]-카테고리-깊이구분.md
new file mode 100644
index 00000000..0cb61da0
--- /dev/null
+++ b/docs/ycshin-node/CTI[맥락]-카테고리-깊이구분.md
@@ -0,0 +1,105 @@
+# [맥락노트] 카테고리 드롭다운 - 3단계 깊이 구분 표시
+
+> 관련 문서: [계획서](./CTI[계획]-카테고리-깊이구분.md) | [체크리스트](./CTI[체크]-카테고리-깊이구분.md)
+
+---
+
+## 왜 이 작업을 하는가
+
+- 품목정보 등록 모달의 "재고단위" 등 카테고리 드롭다운에서 3단계 계층이 시각적으로 구분되지 않음
+- 예: "신22"가 "신2"의 하위인데, "신3", "신4"와 같은 레벨로 보임
+- 사용자가 대분류/중분류/소분류 관계를 파악할 수 없어 잘못된 항목을 선택할 위험
+
+---
+
+## 핵심 결정 사항과 근거
+
+### 1. 원인: HTML 공백 축소(collapse)
+
+- **현상**: `flattenTree`에서 `" ".repeat(depth)`로 들여쓰기를 만들지만, HTML이 연속 공백을 하나로 합침
+- **결과**: depth 1(`" └ "`)과 depth 2(`" └ "`)가 동일하게 렌더링됨
+- **확인**: `SelectItem`, `CommandItem` 모두 `white-space: pre` 미적용 상태
+
+### 2. 해결: Non-Breaking Space(`\u00A0`) 사용
+
+- **결정**: 일반 공백 `" "`를 `"\u00A0"`로 교체
+- **근거**: `\u00A0`는 HTML에서 축소되지 않아 depth별 들여쓰기가 정확히 유지됨
+- **대안 검토**:
+ - `white-space: pre` CSS 적용 → 기각 (SelectItem, CommandItem 양쪽 모두 수정 필요, shadcn 기본 스타일 오버라이드 부담)
+ - CSS `padding-left` 사용 → 기각 (label 문자열 기반 옵션 구조에서 개별 아이템에 스타일 전달 어려움)
+ - 트리 문자(`│`, `├`, `└`) 조합 → 기각 (과도한 시각 정보, 단순 들여쓰기면 충분)
+
+### 3. depth당 3칸 `\u00A0`
+
+- **결정**: `"\u00A0\u00A0\u00A0".repeat(depth)` (depth당 3칸)
+- **근거**: 기존 2칸은 `\u00A0`로 바꿔도 depth간 차이가 작음. 3칸이 시각적 구분에 적절
+
+### 4. 두 파일 동시 수정
+
+- **결정**: `V2Select.tsx`와 `UnifiedSelect.tsx` 모두 수정
+- **근거**: 동일한 `flattenTree` 패턴이 두 컴포넌트에 존재. 하나만 수정하면 불일치 발생
+
+### 5. 기존 prefix strip 정규식 호환
+
+- **확인**: V2Select.tsx 979행의 `o.label.replace(/^[\s└]+/, "").trim()`
+- **근거**: JavaScript `\s`는 `\u00A0`를 포함하므로 추가 수정 불필요
+
+---
+
+## 구현 중 발견한 사항
+
+### CAT_ vs CATEGORY_ 접두사 불일치
+
+테스트 과정에서 최고 관리자 계정으로 리스트 조회 시 `CAT_MMLL6U02_QH2V` 같은 코드가 그대로 표시되는 현상 발견.
+
+- **원인**: 카테고리 값 생성 함수가 두 곳에 존재하며 접두사가 다름
+ - `CategoryValueAddDialog.tsx`: `CATEGORY_` 접두사
+ - `CategoryValueManagerTree.tsx`: `CAT_` 접두사
+- **영향**: 리스트 해석 로직(`V2Repeater`, `InteractiveDataTable`, `UnifiedRepeater`)이 `CATEGORY_` 접두사만 인식하여 `CAT_` 코드는 라벨 변환 실패
+- **판단**: 일반 회사 계정에서는 정상 동작 확인. 본 작업(들여쓰기 표시) 범위 외로 별도 이슈로 분리
+
+---
+
+## 관련 파일 위치
+
+| 구분 | 파일 경로 | 설명 |
+|------|----------|------|
+| 수정 완료 | `frontend/components/v2/V2Select.tsx` | flattenTree 함수 (904행) |
+| 수정 완료 | `frontend/components/unified/UnifiedSelect.tsx` | flattenTree 함수 (632행) |
+| 백엔드 (변경 없음) | `backend-node/src/services/tableCategoryValueService.ts` | buildHierarchy 메서드 |
+| UI 컴포넌트 (변경 없음) | `frontend/components/ui/select.tsx` | SelectItem 렌더링 |
+| UI 컴포넌트 (변경 없음) | `frontend/components/ui/command.tsx` | CommandItem 렌더링 |
+
+---
+
+## 기술 참고
+
+### flattenTree 동작 흐름
+
+```
+백엔드 API 응답 (트리 구조):
+{
+ valueCode: "CAT_001", valueLabel: "신예철", children: [
+ { valueCode: "CAT_002", valueLabel: "신2", children: [
+ { valueCode: "CAT_003", valueLabel: "신22", children: [] }
+ ]},
+ { valueCode: "CAT_004", valueLabel: "신3", children: [] },
+ { valueCode: "CAT_005", valueLabel: "신4", children: [] }
+ ]
+}
+
+→ flattenTree 변환 후 (SelectOption 배열):
+[
+ { value: "CAT_001", label: "신예철" },
+ { value: "CAT_002", label: "\u00A0\u00A0\u00A0└ 신2" },
+ { value: "CAT_003", label: "\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0└ 신22" },
+ { value: "CAT_004", label: "\u00A0\u00A0\u00A0└ 신3" },
+ { value: "CAT_005", label: "\u00A0\u00A0\u00A0└ 신4" }
+]
+```
+
+### value vs label 분리
+
+- `value` (저장값): `valueCode` — DB에 저장되는 값, 들여쓰기 없음
+- `label` (표시값): prefix + `valueLabel` — 화면에만 보이는 값, 들여쓰기 포함
+- 데이터 무결성에 영향 없음
diff --git a/docs/ycshin-node/CTI[체크]-카테고리-깊이구분.md b/docs/ycshin-node/CTI[체크]-카테고리-깊이구분.md
new file mode 100644
index 00000000..8a1cc237
--- /dev/null
+++ b/docs/ycshin-node/CTI[체크]-카테고리-깊이구분.md
@@ -0,0 +1,53 @@
+# [체크리스트] 카테고리 드롭다운 - 3단계 깊이 구분 표시
+
+> 관련 문서: [계획서](./CTI[계획]-카테고리-깊이구분.md) | [맥락노트](./CTI[맥락]-카테고리-깊이구분.md)
+
+---
+
+## 공정 상태
+
+- 전체 진행률: **100%** (완료)
+- 현재 단계: 전체 완료
+
+---
+
+## 구현 체크리스트
+
+### 1단계: 코드 수정
+
+- [x] `V2Select.tsx` 904행 — `flattenTree` prefix를 `\u00A0` 기반으로 변경
+- [x] `UnifiedSelect.tsx` 632행 — 동일한 `flattenTree` prefix 변경
+
+### 2단계: 검증
+
+- [x] depth 1 항목: 3칸 들여쓰기 + `└` 표시 확인
+- [x] depth 2 항목: 6칸 들여쓰기 + `└` 표시, depth 1과 명확히 구분됨 확인
+- [x] depth 0 항목: 들여쓰기 없이 원래대로 표시 확인
+- [x] 항목 선택 후 값이 정상 저장되는지 확인 (valueCode 기준)
+- [x] 기존 prefix strip 로직 정상 동작 확인 — JS `\s`가 `\u00A0` 포함하므로 호환
+- [x] 검색 가능 모드(Combobox): 정상 동작 확인
+- [x] 비검색 모드(Select): 렌더링 정상 확인
+
+### 3단계: 정리
+
+- [x] 린트 에러 없음 확인 (기존 에러 제외)
+- [x] 계맥체 문서 최신화
+
+---
+
+## 참고: 최고 관리자 계정 표시 이슈
+
+- 최고 관리자(`company_code = "*"`)로 리스트 조회 시 `CAT_MMLL6U02_QH2V` 같은 코드값이 그대로 노출되는 현상 발견
+- 원인: `CategoryValueManagerTree.tsx`의 `generateCode()`가 `CAT_` 접두사를 사용하나, 리스트 해석 로직은 `CATEGORY_` 접두사만 인식
+- 일반 회사 계정에서는 정상 표시됨을 확인
+- 본 작업 범위 외로 판단하여 별도 이슈로 분리
+
+---
+
+## 변경 이력
+
+| 날짜 | 내용 |
+|------|------|
+| 2026-03-11 | 계획서, 맥락노트, 체크리스트 작성 |
+| 2026-03-11 | 1단계 코드 수정 완료 (V2Select.tsx, UnifiedSelect.tsx) |
+| 2026-03-11 | 2단계 검증 완료, 3단계 문서 정리 완료 |
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index abcbd2f8..b3dbab89 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -458,6 +458,14 @@ select {
border-color: hsl(var(--destructive)) !important;
}
+
+/* 채번 세그먼트 포커스 스타일 (shadcn Input과 동일한 3단 구조) */
+.numbering-segment:focus-within {
+ box-shadow: 0 0 0 3px hsl(var(--ring) / 0.5);
+ outline: 2px solid hsl(var(--ring));
+ outline-offset: 2px;
+}
+
/* 필수 입력 경고 문구 (입력 필드 아래, 레이아웃 영향 없음) */
.validation-error-msg-wrapper {
height: 0;
diff --git a/frontend/components/table-category/CategoryValueManagerTree.tsx b/frontend/components/table-category/CategoryValueManagerTree.tsx
index 88ecfb49..f6f7ff8a 100644
--- a/frontend/components/table-category/CategoryValueManagerTree.tsx
+++ b/frontend/components/table-category/CategoryValueManagerTree.tsx
@@ -288,6 +288,7 @@ export const CategoryValueManagerTree: React.FC =
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
const [parentValue, setParentValue] = useState(null);
+ const [continuousAdd, setContinuousAdd] = useState(false);
const [editingValue, setEditingValue] = useState(null);
const [deletingValue, setDeletingValue] = useState(null);
@@ -512,21 +513,24 @@ export const CategoryValueManagerTree: React.FC =
const response = await createCategoryValue(input);
if (response.success) {
toast.success("카테고리가 추가되었습니다");
- // 폼 초기화 (모달은 닫지 않고 연속 입력)
- setFormData((prev) => ({
- ...prev,
- valueCode: "",
- valueLabel: "",
- description: "",
- color: "",
- }));
- setTimeout(() => addNameRef.current?.focus(), 50);
- // 기존 펼침 상태 유지하면서 데이터 새로고침
await loadTree(true);
- // 부모 노드만 펼치기 (하위 추가 시)
if (parentValue) {
setExpandedNodes((prev) => new Set([...prev, parentValue.valueId]));
}
+
+ if (continuousAdd) {
+ setFormData((prev) => ({
+ ...prev,
+ valueCode: "",
+ valueLabel: "",
+ description: "",
+ color: "",
+ }));
+ setTimeout(() => addNameRef.current?.focus(), 50);
+ } else {
+ setFormData({ valueCode: "", valueLabel: "", description: "", color: "", isActive: true });
+ setIsAddModalOpen(false);
+ }
} else {
toast.error(response.error || "추가 실패");
}
@@ -818,6 +822,19 @@ export const CategoryValueManagerTree: React.FC =
추가
+
+
+
+ setContinuousAdd(checked as boolean)}
+ />
+
+
+
diff --git a/frontend/components/unified/UnifiedSelect.tsx b/frontend/components/unified/UnifiedSelect.tsx
index d307fbc1..1045ba8c 100644
--- a/frontend/components/unified/UnifiedSelect.tsx
+++ b/frontend/components/unified/UnifiedSelect.tsx
@@ -629,7 +629,7 @@ export const UnifiedSelect = forwardRef((pro
): SelectOption[] => {
const result: SelectOption[] = [];
for (const item of items) {
- const prefix = depth > 0 ? " ".repeat(depth) + "└ " : "";
+ const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
result.push({
value: String(item.valueId), // valueId를 value로 사용 (채번 매핑과 일치)
label: prefix + item.valueLabel,
diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx
index e61ba143..2d7c3246 100644
--- a/frontend/components/v2/V2Input.tsx
+++ b/frontend/components/v2/V2Input.tsx
@@ -909,10 +909,10 @@ export const V2Input = forwardRef((props, ref) =>
const templateSuffix = templateParts.length > 1 ? templateParts.slice(1).join("") : "";
return (
-
+
{/* 고정 접두어 */}
{templatePrefix && (
-
+
{templatePrefix}
)}
@@ -945,13 +945,13 @@ export const V2Input = forwardRef((props, ref) =>
}
}}
placeholder="입력"
- className="h-full min-w-[60px] flex-1 bg-transparent px-2 text-sm focus-visible:outline-none"
+ className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm ring-0"
disabled={disabled || isGeneratingNumbering}
- style={inputTextStyle}
+ style={{ ...inputTextStyle, outline: 'none' }}
/>
{/* 고정 접미어 */}
{templateSuffix && (
-
+
{templateSuffix}
)}
diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx
index 959abe05..9062e7bc 100644
--- a/frontend/components/v2/V2Select.tsx
+++ b/frontend/components/v2/V2Select.tsx
@@ -901,7 +901,7 @@ export const V2Select = forwardRef((props, ref) =
): SelectOption[] => {
const result: SelectOption[] = [];
for (const item of items) {
- const prefix = depth > 0 ? " ".repeat(depth) + "└ " : "";
+ const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
result.push({
value: item.valueCode, // 🔧 valueCode를 value로 사용
label: prefix + item.valueLabel,
diff --git a/frontend/lib/hooks/useDialogAutoValidation.ts b/frontend/lib/hooks/useDialogAutoValidation.ts
index eefa9342..66e4f20b 100644
--- a/frontend/lib/hooks/useDialogAutoValidation.ts
+++ b/frontend/lib/hooks/useDialogAutoValidation.ts
@@ -98,6 +98,16 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
return el instanceof HTMLSelectElement && el.hasAttribute("aria-hidden");
}
+ // 복합 입력 필드(채번 세그먼트 등)의 시각적 테두리 컨테이너 탐지
+ // input 자체에 border가 없고 부모가 border를 가진 경우 부모를 반환
+ function findBorderContainer(input: TargetEl): HTMLElement {
+ const parent = input.parentElement;
+ if (parent && parent.classList.contains("border")) {
+ return parent;
+ }
+ return input;
+ }
+
function isEmpty(input: TargetEl): boolean {
if (input instanceof HTMLButtonElement) {
// Radix Select: data-placeholder 속성이 자식 span에 있으면 미선택 상태
@@ -120,20 +130,24 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
}
function markError(input: TargetEl) {
- input.setAttribute(ERROR_ATTR, "true");
+ const container = findBorderContainer(input);
+ container.setAttribute(ERROR_ATTR, "true");
errorFields.add(input);
showErrorMsg(input);
}
function clearError(input: TargetEl) {
- input.removeAttribute(ERROR_ATTR);
+ const container = findBorderContainer(input);
+ container.removeAttribute(ERROR_ATTR);
errorFields.delete(input);
removeErrorMsg(input);
}
// 빈 필수 필드 아래에 경고 문구 삽입 (레이아웃 영향 없는 zero-height wrapper)
+ // 복합 입력(채번 세그먼트 등)은 border 컨테이너 바깥에 삽입
function showErrorMsg(input: TargetEl) {
- if (input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`)) return;
+ const container = findBorderContainer(input);
+ if (container.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`)) return;
const wrapper = document.createElement("div");
wrapper.className = MSG_WRAPPER_CLASS;
@@ -142,17 +156,19 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
msg.textContent = "필수 입력 항목입니다";
wrapper.appendChild(msg);
- input.insertAdjacentElement("afterend", wrapper);
+ container.insertAdjacentElement("afterend", wrapper);
}
function removeErrorMsg(input: TargetEl) {
- const wrapper = input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`);
+ const container = findBorderContainer(input);
+ const wrapper = container.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`);
if (wrapper) wrapper.remove();
}
function highlightField(input: TargetEl) {
- input.setAttribute(HIGHLIGHT_ATTR, "true");
- input.addEventListener("animationend", () => input.removeAttribute(HIGHLIGHT_ATTR), { once: true });
+ const container = findBorderContainer(input);
+ container.setAttribute(HIGHLIGHT_ATTR, "true");
+ container.addEventListener("animationend", () => container.removeAttribute(HIGHLIGHT_ATTR), { once: true });
if (input instanceof HTMLButtonElement) {
input.click();