@simplysm/sd-claude 14.0.80 → 14.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/claude/references/sd-requirement-source-handling.md +17 -17
  2. package/claude/references/sd-simplysm14/README.md +58 -58
  3. package/claude/references/sd-simplysm14/manuals/client-component.md +739 -739
  4. package/claude/references/sd-simplysm14/manuals/client-crud.md +1 -1
  5. package/claude/references/sd-simplysm14/manuals/client-demo.md +1 -1
  6. package/claude/references/sd-simplysm14/manuals/client-setup.md +2 -2
  7. package/claude/references/sd-simplysm14/manuals/client-tab.md +2 -2
  8. package/claude/references/sd-simplysm14/manuals/logging.md +3 -3
  9. package/claude/references/sd-simplysm14/manuals/orm-union.md +7 -7
  10. package/claude/references/sd-simplysm14/manuals/orm.md +75 -75
  11. package/claude/references/sd-simplysm14/manuals/test.md +8 -8
  12. package/claude/rules/sd-base-rules.md +306 -293
  13. package/claude/rules/sd-design-rules.md +44 -44
  14. package/claude/skills/sd-commit/SKILL.md +17 -17
  15. package/claude/skills/sd-config/SKILL.md +4 -4
  16. package/claude/skills/sd-demo/SKILL.md +40 -48
  17. package/claude/skills/sd-demo/evals/fixtures/inventory-list/.specs/inventory/spec.md +99 -0
  18. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/package.json +12 -0
  19. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/index.ts +3 -0
  20. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inbound/inbound.list.ts +150 -0
  21. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inventory/inventory-master.list.ts +143 -0
  22. package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/outbound/outbound.list.ts +150 -0
  23. package/claude/skills/sd-demo/evals/fixtures/inventory-list/pnpm-workspace.yaml +2 -0
  24. package/claude/skills/sd-demo/evals/fixtures/inventory-list/sd.config.ts +12 -0
  25. package/claude/skills/sd-demo/evals/golden.jsonl +1 -5
  26. package/claude/skills/sd-dev/SKILL.md +49 -22
  27. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/package.json +8 -0
  28. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/src/.gitkeep +0 -0
  29. package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tests/.gitkeep +0 -0
  30. package/claude/skills/sd-dev/evals/fixtures/{case-modify → minimal-ts-pkg}/tsconfig.json +1 -3
  31. package/claude/skills/sd-dev/evals/golden.jsonl +1 -3
  32. package/claude/skills/sd-docs/SKILL.md +8 -8
  33. package/claude/skills/sd-impl/SKILL.md +172 -82
  34. package/claude/skills/sd-impl/evals/fixtures/case-a-new-screen/.specs/260513120000_warehouse/spec.md +101 -0
  35. package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/.specs/260513120000_warehouse/spec.md +101 -0
  36. package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/packages/app/src/screens/box-register/box-register.view.ts +46 -0
  37. package/claude/skills/sd-impl/evals/fixtures/case-c-new-cross/.specs/260513120000_warehouse/spec.md +89 -0
  38. package/claude/skills/sd-impl/evals/fixtures/case-d-spec-modify/.specs/260513120000_warehouse/spec.md +101 -0
  39. package/claude/skills/sd-impl/evals/golden.jsonl +4 -6
  40. package/claude/skills/sd-review/SKILL.md +33 -0
  41. package/claude/skills/sd-review/evals/fixtures/code-review/src/foo.ts +7 -0
  42. package/claude/skills/sd-review/evals/fixtures/doc-review/docs/foo.md +4 -0
  43. package/claude/skills/sd-review/evals/golden.jsonl +2 -0
  44. package/claude/skills/sd-skill/SKILL.md +99 -91
  45. package/claude/skills/sd-skill/evals/fixtures/existing-skill/.claude/skills/todo-format/SKILL.md +14 -0
  46. package/claude/skills/sd-skill/evals/fixtures/new-skill/.gitkeep +0 -0
  47. package/claude/skills/sd-skill/evals/golden.jsonl +2 -0
  48. package/claude/skills/sd-spec/SKILL.md +251 -246
  49. package/claude/skills/sd-spec/references/example-spec.md +1 -1
  50. package/claude/skills/sd-unpack/SKILL.md +83 -83
  51. package/claude/skills/sd-use/SKILL.md +4 -4
  52. package/package.json +1 -1
  53. package/claude/skills/sd-demo/evals/fixtures/empty/.specs/260513120000_warehouse/spec.md +0 -45
  54. package/claude/skills/sd-demo/evals/fixtures/with-existing-screen/.specs/260513120000_warehouse/spec.md +0 -42
  55. package/claude/skills/sd-demo/evals/fixtures/with-existing-screen/packages/app/src/screens/dashboard/dashboard.view.ts +0 -33
  56. package/claude/skills/sd-demo/evals/fixtures/with-master-screen/.specs/260513120000_warehouse/spec.md +0 -45
  57. package/claude/skills/sd-demo/evals/fixtures/with-master-screen/packages/app/src/screens/dashboard/dashboard.view.ts +0 -33
  58. package/claude/skills/sd-demo/evals/fixtures/with-modal/.specs/260513120000_warehouse/spec.md +0 -75
  59. package/claude/skills/sd-demo/evals/fixtures/with-modal/packages/app/src/screens/dashboard/dashboard.view.ts +0 -33
  60. package/claude/skills/sd-demo/evals/fixtures/with-screens/.specs/260513120000_warehouse/spec.md +0 -45
  61. package/claude/skills/sd-demo/evals/fixtures/with-screens/packages/app/src/screens/dashboard/dashboard.view.ts +0 -33
  62. package/claude/skills/sd-dev/evals/fixtures/case-add/package.json +0 -13
  63. package/claude/skills/sd-dev/evals/fixtures/case-add/src/index.ts +0 -10
  64. package/claude/skills/sd-dev/evals/fixtures/case-add/tests/index.test.ts +0 -11
  65. package/claude/skills/sd-dev/evals/fixtures/case-add/tsconfig.json +0 -12
  66. package/claude/skills/sd-dev/evals/fixtures/case-bug/package.json +0 -13
  67. package/claude/skills/sd-dev/evals/fixtures/case-bug/src/index.ts +0 -10
  68. package/claude/skills/sd-dev/evals/fixtures/case-bug/tests/index.test.ts +0 -11
  69. package/claude/skills/sd-dev/evals/fixtures/case-bug/tsconfig.json +0 -12
  70. package/claude/skills/sd-dev/evals/fixtures/case-modify/package.json +0 -13
  71. package/claude/skills/sd-dev/evals/fixtures/case-modify/src/index.ts +0 -10
  72. package/claude/skills/sd-dev/evals/fixtures/case-modify/tests/index.test.ts +0 -11
  73. package/claude/skills/sd-impl/evals/fixtures/case-001-new-screen/spec.md +0 -55
  74. package/claude/skills/sd-impl/evals/fixtures/case-002-auto-process/spec.md +0 -55
  75. package/claude/skills/sd-impl/evals/fixtures/case-003-update-screen/packages/client/src/pages/book-list.ts +0 -22
  76. package/claude/skills/sd-impl/evals/fixtures/case-003-update-screen/spec.md +0 -57
  77. package/claude/skills/sd-impl/evals/fixtures/case-004-ambiguous-spec/spec.md +0 -58
  78. package/claude/skills/sd-impl/evals/fixtures/case-005-id-mismatch/spec.md +0 -52
  79. package/claude/skills/sd-impl/evals/fixtures/case-006-with-reference-units/packages/client/src/pages//352/261/260/353/236/230/354/262/230//352/261/260/353/236/230/354/262/230-/353/252/251/353/241/235.test.ts +0 -10
  80. package/claude/skills/sd-impl/evals/fixtures/case-006-with-reference-units/packages/client/src/pages//352/261/260/353/236/230/354/262/230//352/261/260/353/236/230/354/262/230-/353/252/251/353/241/235.ts +0 -11
  81. package/claude/skills/sd-impl/evals/fixtures/case-006-with-reference-units/packages/server/src/data-access//352/261/260/353/236/230/354/262/230-/354/240/221/352/267/274.ts +0 -12
  82. package/claude/skills/sd-impl/evals/fixtures/case-006-with-reference-units/packages/server/src/models//352/261/260/353/236/230/354/262/230.ts +0 -8
  83. package/claude/skills/sd-impl/evals/fixtures/case-006-with-reference-units/spec.md +0 -77
  84. package/claude/skills/sd-impl/evals/fixtures/case-new/.specs/260514120000_/352/261/260/353/236/230/354/262/230/spec.md +0 -101
  85. package/claude/skills/sd-impl/evals/fixtures/case-update/.specs/260514120000_/352/261/260/353/236/230/354/262/230/spec.md +0 -101
  86. package/claude/skills/sd-impl/evals/fixtures/case-update/src//352/261/260/353/236/230/354/262/230//352/261/260/353/236/230/354/262/230-/353/252/250/353/215/270.txt +0 -1
  87. package/claude/skills/sd-impl/evals/fixtures/case-update/src//352/261/260/353/236/230/354/262/230//352/261/260/353/236/230/354/262/230-/353/252/251/353/241/235.txt +0 -1
  88. package/claude/skills/sd-impl/references/spec-cross-check.md +0 -82
@@ -48,7 +48,7 @@
48
48
  | `#commandTpl` | 상단(또는 modal/control 모드의 명령 영역) 추가 액션 버튼. |
49
49
  | `#bottomCommandTpl` | modal 하단 좌측 영역. modal + selectMode 면 "선택 해제/확인" 과 함께 표시. |
50
50
 
51
- `<sd-sheet-column>` 은 `<sd-crud-list>` 의 직속 자식으로 두면 내부 시트로 자동 투영된다.
51
+ `<sd-sheet-column>` 은 `<sd-crud-list>` 의 직속 자식으로 두면 내부 시트로 자동 투영됨.
52
52
 
53
53
  ### viewType 별 동작
54
54
 
@@ -1,6 +1,6 @@
1
1
  # 클라이언트 데모 작성 매뉴얼
2
2
 
3
- `@simplysm/angular` v14 로 spec.md §4.x 화면을 **데모** 로 옮길 때의 추가 처방. 컴포넌트 일반 규약(파일명·시그널·DI·핸들러·시트·폼·버튼·모달 호출·합성 패턴 등)은 [client-component.md](./client-component.md). 본 문서는 spec § → 산출물 매핑과 데모 한정 패턴만 다룬다.
3
+ `@simplysm/angular` v14 로 spec.md §4.x 화면을 **데모** 로 옮길 때의 추가 처방. 컴포넌트 일반 규약(파일명·시그널·DI·핸들러·시트·폼·버튼·모달 호출·합성 패턴 등)은 [client-component.md](./client-component.md). 본 문서는 spec § → 산출물 매핑과 데모 한정 패턴만 다룸.
4
4
 
5
5
  ## §4.x 화면 유형 → 파일 역할
6
6
 
@@ -1,10 +1,10 @@
1
1
  # 클라이언트 환경 셋업 매뉴얼
2
2
 
3
- 화면 작성 시점에는 거의 건드리지 않으며, 새 앱 부트스트랩이나 새 서비스/마스터 데이터 추가 시에만 본다.
3
+ 화면 작성 시점에는 거의 건드리지 않으며, 새 앱 부트스트랩이나 새 서비스/마스터 데이터 추가 시에만 봄.
4
4
 
5
5
  ## AppServiceProvider
6
6
 
7
- `@simplysm/service-client` 위에 앱이 만드는 root provider. 서비스/이벤트 프록시와 ORM 커넥터를 lazy 캐싱으로 노출한다.
7
+ `@simplysm/service-client` 위에 앱이 만드는 root provider. 서비스/이벤트 프록시와 ORM 커넥터를 lazy 캐싱으로 노출함.
8
8
 
9
9
  ```ts
10
10
  @Injectable({ providedIn: "root" })
@@ -1,6 +1,6 @@
1
1
  # 탭 컨트롤 사용법
2
2
 
3
- `<sd-tab>` 은 **선택기**다. 콘텐츠 컨테이너가 아니다. 콘텐츠는 탭 바깥에서 `@if` / `@switch` 로 분기 렌더한다.
3
+ `<sd-tab>` 은 **선택기**. 콘텐츠 컨테이너가 아님. 콘텐츠는 탭 바깥에서 `@if` / `@switch` 로 분기 렌더함.
4
4
 
5
5
  ## API
6
6
 
@@ -9,7 +9,7 @@
9
9
  | `<sd-tab>` | `[(value)]` (model) | 현재 선택값. 양방향 필수. |
10
10
  | `<sd-tab-item>` | `[value]` (input) | 이 항목의 식별값. 클릭 시 부모로 set. |
11
11
 
12
- 선택 상태는 `sd-tab-item` 이 부모 `sd-tab` 의 `value` 와 자기 `value` 를 비교해 자동 결정한다.
12
+ 선택 상태는 `sd-tab-item` 이 부모 `sd-tab` 의 `value` 와 자기 `value` 를 비교해 자동 결정함.
13
13
 
14
14
  ## 표준 패턴
15
15
 
@@ -49,7 +49,7 @@ console.error("[X] 실패:", err);
49
49
 
50
50
  ## 모듈-레벨 logger 주의
51
51
 
52
- 모듈 레벨에서 `consola.withTag()` 를 직접 호출하면 호출 시점의 options(level/reporters)가 스냅샷으로 고정되어, 이후 `setupConsola()` 가 reporters 를 갱신해도 child instance 에 반영되지 않는다.
52
+ 모듈 레벨에서 `consola.withTag()` 를 직접 호출하면 호출 시점의 options(level/reporters)가 스냅샷으로 고정되어, 이후 `setupConsola()` 가 reporters 를 갱신해도 child instance 에 반영되지 않음.
53
53
 
54
54
  - **해결**: `createLogger(tag)` 사용 (`@simplysm/core-common`, 내부 구현은 lazy Proxy — 첫 메서드 접근 시점까지 `withTag` 생성을 지연).
55
55
  - 모든 환경(Node·브라우저·Capacitor)에서 위치(모듈 레벨·함수 내부·class field)에 관계없이 `createLogger` 로 통일.
@@ -57,9 +57,9 @@ console.error("[X] 실패:", err);
57
57
 
58
58
  ## 예외 — `eslint-disable no-console` 가 정당화되는 자리
59
59
 
60
- 다음 경우에 한해 `/* eslint-disable no-console */` 파일 헤더를 허용한다. 그 외는 모두 consola 로 교체.
60
+ 다음 경우에 한해 `/* eslint-disable no-console */` 파일 헤더를 허용함. 그 외는 모두 consola 로 교체.
61
61
 
62
62
  - **CLI 도움말·yargs help 텍스트** 처럼 stdout 그 자체를 사용자 출력으로 쓰는 경우 (예: `packages/sd-cli/src/sd-cli-entry.ts` 의 `collectYargsHelp`).
63
63
  - **ErrorHandler 마지막 안전망** 등 consola 자체가 죽었을 가능성이 있는 catch 블록 — 해당 자리만 `eslint-disable-next-line no-console` + 이유 주석.
64
64
 
65
- 예외 적용 시 disable 주석 위에 사유를 1줄로 남긴다.
65
+ 예외 적용 시 disable 주석 위에 사유를 1줄로 남김.
@@ -1,15 +1,15 @@
1
1
  # ORM UNION 사용법
2
2
 
3
- [단일 쿼리 우선](./orm.md) 규칙을 지키면서 **서로 다른 엔티티를 한 목록으로** 보여줘야 할 때 쓴다. 코드에서 merge 하지 말고, select shape 을 동일하게 맞춘 두 Queryable 을 `Queryable.union(...)` 으로 합친다.
3
+ [단일 쿼리 우선](./orm.md) 규칙을 지키면서 **서로 다른 엔티티를 한 목록으로** 보여줘야 할 때 씀. 코드에서 merge 하지 말고, select shape 을 동일하게 맞춘 두 Queryable 을 `Queryable.union(...)` 으로 합침.
4
4
 
5
5
  ## 규칙
6
6
 
7
- - 두 쿼리의 select 컬럼 이름·타입·순서가 정확히 일치해야 한다.
8
- - 행 종류 구분 등 리터럴 값은 JS 값을 그대로 쓴다.
9
- - 한쪽에만 있는 컬럼은 NULL 자리채움이 필요한데 타입 추론이 안 되므로 `expr.raw("<type>")\`NULL\`` 로 명시한다.
10
- - 필터(`where`)는 union 전에 각 쿼리에 같은 조건으로 적용한다 — 각 소스에서 미리 행 수를 줄여야 union 비용이 작다 (predicate pushdown). union 결과에 필터를 걸면 두 소스를 다 읽은 뒤 거르게 되고, select 한 컬럼만 남아 join 컬럼으로 필터도 불가하다.
11
- - 총 건수는 가능하면 **각 소스에서 따로 `count` 후 합산**한다. union 결과의 `count` 는 양쪽 SELECT 를 모두 실행해 머티리얼라이즈한 뒤 세지만, 각각 세면 단순 집계로 끝난다. 중복 제거가 필요해 합산이 부정확해질 때만 union 후 count.
12
- - `Queryable.union(...)` 결과는 의미상 wrap 된 derived table 로, 그 위에서 호출되는 모든 fluent 연산자(`orderBy` / `limit` / `top` / `distinct` / `groupBy` / `having` / `where` / `select` / `join` 등)는 외부 union 위에 자동 적용된다. sub 별 적용(predicate pushdown 등)을 원하면 union 전에 각 sub-Queryable 에 미리 호출한다.
7
+ - 두 쿼리의 select 컬럼 이름·타입·순서가 정확히 일치해야 함.
8
+ - 행 종류 구분 등 리터럴 값은 JS 값을 그대로 씀.
9
+ - 한쪽에만 있는 컬럼은 NULL 자리채움이 필요한데 타입 추론이 안 되므로 `expr.raw("<type>")\`NULL\`` 로 명시함.
10
+ - 필터(`where`)는 union 전에 각 쿼리에 같은 조건으로 적용함 — 각 소스에서 미리 행 수를 줄여야 union 비용이 작음 (predicate pushdown). union 결과에 필터를 걸면 두 소스를 다 읽은 뒤 거르게 되고, select 한 컬럼만 남아 join 컬럼으로 필터도 불가함.
11
+ - 총 건수는 가능하면 **각 소스에서 따로 `count` 후 합산**함. union 결과의 `count` 는 양쪽 SELECT 를 모두 실행해 머티리얼라이즈한 뒤 세지만, 각각 세면 단순 집계로 끝남. 중복 제거가 필요해 합산이 부정확해질 때만 union 후 count.
12
+ - `Queryable.union(...)` 결과는 의미상 wrap 된 derived table 로, 그 위에서 호출되는 모든 fluent 연산자(`orderBy` / `limit` / `top` / `distinct` / `groupBy` / `having` / `where` / `select` / `join` 등)는 외부 union 위에 자동 적용됨. sub 별 적용(predicate pushdown 등)을 원하면 union 전에 각 sub-Queryable 에 미리 호출함.
13
13
 
14
14
  ## 예시
15
15
 
@@ -1,75 +1,75 @@
1
- # ORM 작업 가이드
2
-
3
- ## 단일 쿼리 우선
4
-
5
- 연관 데이터는 select 의 `join` 으로 한 쿼리에 모아 가져온다. 여러 쿼리로 나눠 받아 코드(서버/UI)에서 합치지 않는다.
6
-
7
- ORM 빌더로 표현 불가능한 경우, 작성 전 사용자 보고 후 중단.
8
-
9
- 예외: raw query로도 표현 불가하거나 단일 쿼리화가 명백히 비효율일 때만. 사유를 코드 주석에 남긴다.
10
-
11
- 이종 엔티티(예: 입고 + 출고)를 한 목록으로 보여줘야 할 때 두 결과를 코드에서 merge 하지 않으려면 → [orm-union.md](./orm-union.md) 참조.
12
-
13
- ## 조회 목록 표준 흐름
14
-
15
- list / sheet 화면의 본 쿼리는 다음 순서로 작성한다:
16
-
17
- 1. root queryable 빌드 (`db.X()`).
18
- 2. 필요한 연관 데이터를 `joinSingle` 로 부착 — 본 행에 곧장 부착하지 않으면 안 되는 컬럼만. join 내부 쿼리도 동일 흐름 (`from → joinSingle → where → select`).
19
- 3. `.select((p) => ({ ...도출 컬럼들... }))` — coalesce / CASE WHEN / 산식 등 모든 도출을 한 번에 projection. select 콜백 안에서 로컬 `const` 로 산식을 잘게 나눠 가독성 확보.
20
- 4. WHERE 는 step 3 의 projected 컬럼 이름으로 직접 참조 (`r.psd`, `r.isCanceled`, `r.status`, …). framework 가 projected ExprUnit AST 를 WHERE 절에 그대로 inline 하므로 wrap 없이도 도출 컬럼 필터링이 동작한다.
21
- 5. `count()` 로 총 건수.
22
- 6. `orderBy(...).limit(page * size, size).execute()`.
23
-
24
- WHERE / SELECT 양쪽에서 동일 도출 산식을 쓰겠다고 `buildDerived(p)` 같은 helper 함수 만들지 말 것 — step 3 의 projected 컬럼이 자동으로 그 역할을 한다.
25
-
26
- 화면 첫 진입 1회만 필요하고 refresh / 필터 변경에 무관한 데이터 (필터 dropdown 옵션 등) 는 본 목록 쿼리에 섞지 말 것. 별도 1회 effect 로 분리해 init 시점에만 로드한다.
27
-
28
- ## 안티패턴
29
-
30
- ### SELECT 절 안 `expr.subquery` / `expr.exists` 박지 말 것
31
-
32
- 도메인 boolean (`isCompleted`, `hasAny` 등) / 집계 (`SUM`, `COUNT`, `MAX`) 가 필요하면 `joinSingle` 안에서 `from + where + select(aggregate)` 로 묶어 outer 행에 컬럼으로 부착한다. SELECT column 에 subquery / exists 박으면 outer 행마다 inner 가 N 회 실행된다.
33
-
34
- ```ts
35
- // 나쁜 예 — 행당 subquery N회
36
- .select((p) => ({
37
- isCompleted: expr.is(expr.exists(db.X().where(...))),
38
- sumA: expr.subquery("number", db.Y().select(...)),
39
- }))
40
-
41
- // 좋은 예 — joinSingle 로 1회 부착, 컬럼으로 참조
42
- .joinSingle("state", (q, p) =>
43
- q.from(X).where((x) => [expr.eq(x.fk, p.id)])
44
- .select((x) => ({
45
- rowCount: expr.count(),
46
- completedCount: expr.count(x.completedAt),
47
- sumA: expr.sum(x.amount),
48
- })),
49
- )
50
- .select((p) => ({
51
- isCompleted: expr.gt(p.state!.completedCount, 0),
52
- sumA: expr.coalesce(p.state!.sumA, 0),
53
- }))
54
- ```
55
-
56
- ### 불필요한 `wrap()`
57
-
58
- `wrap()` 은 framework 가 명시 요구하는 경우에만 쓴다 (대표적으로 `count()` after `distinct()` / `groupBy()` 호출 같은 명시 요구).
59
-
60
- 도출 컬럼 위에서 필터/정렬을 걸기 위해 wrap 을 끼우는 패턴은 불필요 — `.select(...).where((r) => [...])` 만으로 framework 가 projected ExprUnit AST 를 WHERE / ORDER BY 에 inline 한다.
61
-
62
- "Layer 1 = materialize, Layer 2 = derive" 같은 다층 wrap 구조도 군더더기. 단일 select 안 로컬 `const` 로 산식 분리하면 동일 SQL 이 나온다.
63
-
64
- ## 스키마 정의
65
-
66
- 컬럼은 `NOT NULL` 기본. `.nullable()`/`.default(...)` 는 도메인 근거가 있을 때만 붙인다.
67
-
68
- - `.nullable()`: 도메인상 값이 없을 수 있을 때만 (선택 입력, 미발생 이벤트 시각, 선택적 FK).
69
- - `.default(...)`: 사용자가 명시 지시한 경우에만.
70
- - "초기값 애매", "마이그레이션 중간 단계", "넣을 값 모름" 은 nullable/default 근거 아님. 호출자가 넣도록 강제하거나 backfill 후 `NOT NULL`로 전환.
71
-
72
- ## 삭제 전략
73
-
74
- - **기초정보(마스터)**: soft delete (`isDisabled` 등). FK 참조 무결성 보존.
75
- - **프로세스 문서(트랜잭션)**: 물리 delete. 상세 행 포함 캐스케이드. 단, 다른 테이블이 FK로 참조 중이면 삭제 차단하고 최종 사용자에게 toast 등으로 사유 안내.
1
+ # ORM 작업 가이드
2
+
3
+ ## 단일 쿼리 우선
4
+
5
+ 연관 데이터는 select 의 `join` 으로 한 쿼리에 모아 가져옴. 여러 쿼리로 나눠 받아 코드(서버/UI)에서 합치지 않음.
6
+
7
+ ORM 빌더로 표현 불가능한 경우, 작성 전 사용자 보고 후 중단.
8
+
9
+ 예외: raw query로도 표현 불가하거나 단일 쿼리화가 명백히 비효율일 때만. 사유를 코드 주석에 남김.
10
+
11
+ 이종 엔티티(예: 입고 + 출고)를 한 목록으로 보여줘야 할 때 두 결과를 코드에서 merge 하지 않으려면 → [orm-union.md](./orm-union.md) 참조.
12
+
13
+ ## 조회 목록 표준 흐름
14
+
15
+ list / sheet 화면의 본 쿼리는 다음 순서로 작성:
16
+
17
+ 1. root queryable 빌드 (`db.X()`).
18
+ 2. 필요한 연관 데이터를 `joinSingle` 로 부착 — 본 행에 곧장 부착하지 않으면 안 되는 컬럼만. join 내부 쿼리도 동일 흐름 (`from → joinSingle → where → select`).
19
+ 3. `.select((p) => ({ ...도출 컬럼들... }))` — coalesce / CASE WHEN / 산식 등 모든 도출을 한 번에 projection. select 콜백 안에서 로컬 `const` 로 산식을 잘게 나눠 가독성 확보.
20
+ 4. WHERE 는 step 3 의 projected 컬럼 이름으로 직접 참조 (`r.psd`, `r.isCanceled`, `r.status`, …). framework 가 projected ExprUnit AST 를 WHERE 절에 그대로 inline 하므로 wrap 없이도 도출 컬럼 필터링이 동작함.
21
+ 5. `count()` 로 총 건수.
22
+ 6. `orderBy(...).limit(page * size, size).execute()`.
23
+
24
+ WHERE / SELECT 양쪽에서 동일 도출 산식을 쓰겠다고 `buildDerived(p)` 같은 helper 함수 만들지 말 것 — step 3 의 projected 컬럼이 자동으로 그 역할을 함.
25
+
26
+ 화면 첫 진입 1회만 필요하고 refresh / 필터 변경에 무관한 데이터 (필터 dropdown 옵션 등) 는 본 목록 쿼리에 섞지 말 것. 별도 1회 effect 로 분리해 init 시점에만 로드함.
27
+
28
+ ## 안티패턴
29
+
30
+ ### SELECT 절 안 `expr.subquery` / `expr.exists` 박지 말 것
31
+
32
+ 도메인 boolean (`isCompleted`, `hasAny` 등) / 집계 (`SUM`, `COUNT`, `MAX`) 가 필요하면 `joinSingle` 안에서 `from + where + select(aggregate)` 로 묶어 outer 행에 컬럼으로 부착함. SELECT column 에 subquery / exists 박으면 outer 행마다 inner 가 N 회 실행됨.
33
+
34
+ ```ts
35
+ // 나쁜 예 — 행당 subquery N회
36
+ .select((p) => ({
37
+ isCompleted: expr.is(expr.exists(db.X().where(...))),
38
+ sumA: expr.subquery("number", db.Y().select(...)),
39
+ }))
40
+
41
+ // 좋은 예 — joinSingle 로 1회 부착, 컬럼으로 참조
42
+ .joinSingle("state", (q, p) =>
43
+ q.from(X).where((x) => [expr.eq(x.fk, p.id)])
44
+ .select((x) => ({
45
+ rowCount: expr.count(),
46
+ completedCount: expr.count(x.completedAt),
47
+ sumA: expr.sum(x.amount),
48
+ })),
49
+ )
50
+ .select((p) => ({
51
+ isCompleted: expr.gt(p.state!.completedCount, 0),
52
+ sumA: expr.coalesce(p.state!.sumA, 0),
53
+ }))
54
+ ```
55
+
56
+ ### 불필요한 `wrap()`
57
+
58
+ `wrap()` 은 framework 가 명시 요구하는 경우에만 (대표적으로 `count()` after `distinct()` / `groupBy()` 호출 같은 명시 요구).
59
+
60
+ 도출 컬럼 위에서 필터/정렬을 걸기 위해 wrap 을 끼우는 패턴은 불필요 — `.select(...).where((r) => [...])` 만으로 framework 가 projected ExprUnit AST 를 WHERE / ORDER BY 에 inline 함.
61
+
62
+ "Layer 1 = materialize, Layer 2 = derive" 같은 다층 wrap 구조도 군더더기. 단일 select 안 로컬 `const` 로 산식 분리하면 동일 SQL 이 나옴.
63
+
64
+ ## 스키마 정의
65
+
66
+ 컬럼은 `NOT NULL` 기본. `.nullable()`/`.default(...)` 는 도메인 근거가 있을 때만 붙임.
67
+
68
+ - `.nullable()`: 도메인상 값이 없을 수 있을 때만 (선택 입력, 미발생 이벤트 시각, 선택적 FK).
69
+ - `.default(...)`: 사용자가 명시 지시한 경우에만.
70
+ - "초기값 애매", "마이그레이션 중간 단계", "넣을 값 모름" 은 nullable/default 근거 아님. 호출자가 넣도록 강제하거나 backfill 후 `NOT NULL`로 전환.
71
+
72
+ ## 삭제 전략
73
+
74
+ - **기초정보(마스터)**: soft delete (`isDisabled` 등). FK 참조 무결성 보존.
75
+ - **프로세스 문서(트랜잭션)**: 물리 delete. 상세 행 포함 캐스케이드. 단, 다른 테이블이 FK로 참조 중이면 삭제 차단하고 최종 사용자에게 toast 등으로 사유 안내.
@@ -1,13 +1,13 @@
1
1
  # 테스트 작성
2
2
 
3
- `@simplysm/*` v14 모노레포의 패키지 테스트(`packages/<pkg>/tests/`)와 통합 테스트(`tests/<name>/`) 작성 시 따른다. Vitest project 구성·실행 명령은 루트 `CLAUDE.md` 의 "Vitest 프로젝트 구조" 참조 — 여기서는 작성 규약만 다룬다.
3
+ `@simplysm/*` v14 모노레포의 패키지 테스트(`packages/<pkg>/tests/`)와 통합 테스트(`tests/<name>/`) 작성 시 따름. Vitest project 구성·실행 명령은 루트 `CLAUDE.md` 의 "Vitest 프로젝트 구조" 참조 — 여기서는 작성 규약만 다룸.
4
4
 
5
5
  ## 파일 규약
6
6
 
7
- - 위치
8
- - 패키지: `packages/<pkg>/tests/**/*.spec.ts`
9
- - 통합: `tests/<name>/src/**/*.spec.ts`
10
- - 확장자
7
+ - 위치.
8
+ - 패키지: `packages/<pkg>/tests/**/*.spec.ts`.
9
+ - 통합: `tests/<name>/src/**/*.spec.ts`.
10
+ - 확장자.
11
11
  - `*.spec.ts` — vitest 실행 대상.
12
12
  - `*.acc.spec.ts` — Acceptance 단위 spec. 동일 project 에서 함께 실행.
13
13
  - `*.verify.md` — LLM 수동 검증 항목. vitest 실행 대상 아님. 자동화로 잡기 어려운 검증(JSDoc 표현·문서 일관성 등)에만.
@@ -29,8 +29,8 @@
29
29
  1. `tests/<name>/package.json` — `name: "@simplysm-test/<name>"`, `"private": true`, `"type": "module"`. 필요한 `@simplysm/*` 는 `workspace:*` 로 devDependency 등재.
30
30
  2. `tests/<name>/tsconfig.json` — `extends: "../../tsconfig.json"`, `compilerOptions.typeRoots: ["./node_modules/@types"]`.
31
31
  3. `tests/<name>/src/**/*.spec.ts` — 스펙.
32
- 4. `vitest.config.ts` `projects[]` 에 entry 추가
33
- - `name: "<name>"`, `include: ["tests/<name>/**/*.spec.ts"]`
32
+ 4. `vitest.config.ts` `projects[]` 에 entry 추가.
33
+ - `name: "<name>"`, `include: ["tests/<name>/**/*.spec.ts"]`.
34
34
  - 외부 자원 기동 필요시 `globalSetup: "./tests/<name>/vitest.setup.ts"` + `setup`/`teardown` export.
35
35
  - 외부 자원이 단일 인스턴스 공유면 `fileParallelism: false` (스펙 파일 직렬 실행).
36
36
  - 브라우저 런타임 필요시 `browser: { provider: playwright(), enabled: true, headless: true, instances: [{ browser: "chromium", viewport: { width: 1920, height: 1080 } }] }`.
@@ -43,7 +43,7 @@
43
43
 
44
44
  - `setup({ provide })` 에서 외부 자원 기동(DB 컨테이너·테스트 서버 등). 동적 값(랜덤 포트 등)은 `provide("key", value)` 로 스펙에 전달.
45
45
  - `teardown()` 에서 정리. 실패해도 다음 실행이 망가지지 않도록 best-effort.
46
- - 타입 보강: 파일 상단에
46
+ - 타입 보강: 파일 상단에.
47
47
  ```ts
48
48
  declare module "vitest" {
49
49
  export interface ProvidedContext { key: T }