@simplysm/sd-claude 14.0.76 → 14.0.78

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 (66) hide show
  1. package/claude/output-styles/sd-tone.md +128 -0
  2. package/claude/references/sd-simplysm14/apis/angular/README.md +28 -89
  3. package/claude/references/sd-simplysm14/apis/angular/app-structure.md +75 -32
  4. package/claude/references/sd-simplysm14/apis/angular/buttons.md +65 -29
  5. package/claude/references/sd-simplysm14/apis/angular/crud.md +86 -21
  6. package/claude/references/sd-simplysm14/apis/angular/forms.md +168 -42
  7. package/claude/references/sd-simplysm14/apis/angular/infrastructure.md +200 -49
  8. package/claude/references/sd-simplysm14/apis/angular/kanban.md +64 -20
  9. package/claude/references/sd-simplysm14/apis/angular/layout.md +75 -30
  10. package/claude/references/sd-simplysm14/apis/angular/modal.md +92 -40
  11. package/claude/references/sd-simplysm14/apis/angular/routing.md +86 -25
  12. package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +72 -41
  13. package/claude/references/sd-simplysm14/apis/angular/shared-data.md +113 -21
  14. package/claude/references/sd-simplysm14/apis/angular/sheet.md +108 -33
  15. package/claude/references/sd-simplysm14/apis/angular/toast.md +81 -30
  16. package/claude/references/sd-simplysm14/apis/angular/visual.md +140 -32
  17. package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +46 -43
  18. package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +59 -48
  19. package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +17 -7
  20. package/claude/references/sd-simplysm14/apis/core-common/README.md +43 -116
  21. package/claude/references/sd-simplysm14/apis/core-common/extensions.md +74 -109
  22. package/claude/references/sd-simplysm14/apis/core-common/features.md +40 -35
  23. package/claude/references/sd-simplysm14/apis/core-common/types.md +80 -106
  24. package/claude/references/sd-simplysm14/apis/core-common/utils.md +142 -111
  25. package/claude/references/sd-simplysm14/apis/core-node/README.md +7 -16
  26. package/claude/references/sd-simplysm14/apis/core-node/consola.md +33 -38
  27. package/claude/references/sd-simplysm14/apis/core-node/cpx.md +25 -33
  28. package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +27 -38
  29. package/claude/references/sd-simplysm14/apis/core-node/fsx.md +32 -60
  30. package/claude/references/sd-simplysm14/apis/core-node/pathx.md +14 -45
  31. package/claude/references/sd-simplysm14/apis/core-node/worker.md +35 -81
  32. package/claude/references/sd-simplysm14/apis/excel/README.md +178 -80
  33. package/claude/references/sd-simplysm14/apis/lint/README.md +5 -0
  34. package/claude/references/sd-simplysm14/apis/orm-node/README.md +1 -1
  35. package/claude/references/sd-simplysm14/apis/sd-claude/README.md +28 -5
  36. package/claude/references/sd-simplysm14/apis/sd-cli/README.md +1 -1
  37. package/claude/references/sd-simplysm14/apis/service-client/README.md +57 -50
  38. package/claude/references/sd-simplysm14/apis/service-server/README.md +8 -15
  39. package/claude/references/sd-simplysm14/apis/service-server/auth.md +24 -16
  40. package/claude/references/sd-simplysm14/apis/service-server/builtin-services.md +55 -31
  41. package/claude/references/sd-simplysm14/apis/service-server/define-service.md +28 -44
  42. package/claude/references/sd-simplysm14/apis/service-server/internals.md +59 -18
  43. package/claude/references/sd-simplysm14/apis/service-server/server.md +37 -46
  44. package/claude/references/sd-simplysm14/manuals/client-component.md +3 -1
  45. package/claude/references/sd-simplysm14/manuals/logging.md +9 -8
  46. package/claude/rules/sd-base-rules.md +377 -219
  47. package/claude/settings.json +1 -0
  48. package/claude/skills/sd-commit/SKILL.md +31 -8
  49. package/claude/skills/sd-docs/SKILL.md +15 -10
  50. package/claude/skills/sd-docs/references/subagent-prompt.md +26 -8
  51. package/claude/skills/sd-impl/SKILL.md +1 -1
  52. package/claude/skills/sd-skill/references/skill-authoring.md +1 -1
  53. package/claude/skills/sd-spec/SKILL.md +22 -13
  54. package/claude/skills/sd-spec/references/spec-authoring.md +1 -1
  55. package/claude/skills/sd-unpack/SKILL.md +150 -26
  56. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/_common.cpython-314.pyc +0 -0
  57. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/eml_handler.cpython-314.pyc +0 -0
  58. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/office_com.cpython-314.pyc +0 -0
  59. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/pdf_handler.cpython-314.pyc +0 -0
  60. package/claude/skills/sd-unpack/scripts/handlers/_common.py +17 -2
  61. package/claude/skills/sd-unpack/scripts/handlers/eml_handler.py +100 -24
  62. package/claude/skills/sd-unpack/scripts/handlers/msg_handler.py +140 -27
  63. package/claude/skills/sd-unpack/scripts/handlers/office_com.py +698 -107
  64. package/claude/skills/sd-unpack/scripts/handlers/office_worker.py +34 -26
  65. package/claude/skills/sd-unpack/scripts/handlers/pdf_handler.py +130 -8
  66. package/package.json +1 -1
@@ -1,41 +1,82 @@
1
- # @simplysm/service-server — internals
1
+ # @simplysm/service-server — 내부 전송 계층
2
2
 
3
- `ServiceServer.listen()` 이 자동으로 endpoint 연결하는 핸들러·소켓·프로토콜·레거시 유틸의 표면. 표준 부트스트랩에서는 호출할 필요가 없으며, 자체 Fastify 인스턴스에 라우트를 직접 부착하거나 비표준 전송을 만들 때만 직접 import 한다.
3
+ `ServiceServer.listen()` 이 자동 결선하므로 일반 사용에선 불필요. 커스텀 Fastify 라우트·테스트·비표준 전송 참고.
4
4
 
5
- ## HTTP 핸들러
5
+ ## HTTP
6
6
 
7
7
  ### `handleHttpRequest(req, reply, jwtSecret, runMethod)`
8
8
 
9
- `/api/:service/:method` 라우트 핸들러. `x-sd-client-name` 헤더 필수. `Authorization: Bearer <token>` 있으면 `verifyJwt` 검증 (실패 401). GET 은 `?json=<배열>`, POST 는 본문이 배열이어야 함. 결과를 `runMethod({serviceName, methodName, params, http})` 로 위임.
9
+ `/api/:service/:method` 라우트 핸들러.
10
+
11
+ - `x-sd-client-name` 헤더 필수(없으면 throw).
12
+ - `Authorization: Bearer <jwt>` 있고 `jwtSecret` 도 있으면 `verifyJwt`. 토큰만 있고 secret 없으면 throw. 검증 실패 시 401 응답.
13
+ - GET: `?json=<JSON encoded array>` → `json.parse`.
14
+ - POST: body 가 배열이어야 함(아니면 400).
15
+ - 그 외 메서드: 405.
16
+ - `runMethod({ serviceName, methodName, params, http: { clientName, authTokenPayload } })` 호출 후 결과를 `reply.send`.
10
17
 
11
18
  ### `handleUpload(req, reply, rootPath, jwtSecret)`
12
19
 
13
- multipart 업로드. `Authorization` 필수 (없거나 검증 실패 시 401). 각 파일을 `<rootPath>/www/uploads/<uuid><ext>` 로 저장 `ServiceUploadResult[]` 반환. 중간 에러 시 이미 저장된 모든 파일 삭제 후 500.
20
+ multipart 업로드. `Authorization` 헤더 필수(없으면 401, `jwtSecret` 미구성 시 401). 각 파일을 `<rootPath>/www/uploads/<uuid><ext>` 로 저장. 응답 `ServiceUploadResult[] = { path, filename, size }[]` (`path` = `uploads/<saveName>`). 도중 truncated 또는 에러 발생 진행 중 + 이미 저장된 모든 파일 삭제 후 500.
14
21
 
15
22
  ### `handleStaticFile(req, reply, rootPath, urlPath)`
16
23
 
17
- `<rootPath>/www/<urlPath>` 정적 서빙. 경로 탐색 가드(`pathx.isChildPath`), 디렉토리 슬래시 리다이렉트 후 `index.html` 폴백, 숨김 파일(`.` 시작) 403, ENOENT 404, 기타 500.
24
+ `<rootPath>/www/<urlPath>` 정적 서빙.
25
+
26
+ - 경로 탐색 차단: `pathx.isChildPath(targetFilePath, allowedRootPath)` 가드.
27
+ - 디렉터리 + URL 미 trailing slash → `pathname + "/"` 리다이렉트. trailing slash 있으면 `<dir>/index.html` 사용.
28
+ - 숨김 파일(basename 이 `.` 시작) → 403.
29
+ - ENOENT 404, 기타 500. 에러는 HTML 응답.
30
+
31
+ ## WebSocket
32
+
33
+ ### `createWebSocketHandler(runMethod, jwtSecret): WebSocketHandler`
34
+
35
+ 여러 WS 연결 풀 관리. 처리 메시지:
36
+
37
+ - `<service>.<method>` (body 가 배열) → RPC. `runMethod` 위임.
38
+ - `evt:add { key, name, info }` — 이벤트 리스너 등록.
39
+ - `evt:remove { key }` — 제거.
40
+ - `evt:gets { name }` — 모든 소켓의 해당 이벤트 리스너 정보 조회.
41
+ - `evt:emit { keys, data }` — 매칭 키 가진 소켓에게 `evt:on` 푸시.
42
+ - `auth <token>` — JWT 검증 후 `socket.authTokenPayload` 설정.
43
+
44
+ 에러 코드: `BAD_MESSAGE`(알 수 없는 요청), `INTERNAL_ERROR`. `env("DEV")` truthy 시 `stack` 포함.
45
+
46
+ `addSocket(socket, clientId, clientName, connReq)` 동일 clientId 이전 연결 자동 종료 후 교체. `emit(name, infoSelector, data)` 는 `ServiceServer.emitEvent` 의 백엔드.
47
+
48
+ ### `createServiceSocket(socket, clientId, clientName, connReq): ServiceSocket`
49
+
50
+ 단일 WS 관리.
51
+
52
+ - 5초 ping/pong (`socket.ping()` → 응답 없으면 `terminate()`).
53
+ - 1바이트 `0x01`(ping) 수신 → `0x02`(pong) 송신.
54
+ - 메시지는 `createServerProtocolWrapper()` 통과. 진행률(`type === "progress"`) 디코드 결과는 자동으로 `progress` 메시지 회신.
18
55
 
19
- ## WebSocket / 소켓
56
+ 표면: `connectedAtDateTime: DateTime`, `clientName`, `connReq`, `authTokenPayload`(get/set), `close()`, `send(uuid, msg)`(전송 바이트), `addListener(key, eventName, info)`, `removeListener(key)`, `getEventListeners(eventName)`, `filterEventTargetKeys(targetKeys)`, `on("error"|"close"|"message", handler)`.
20
57
 
21
- ### `createWebSocketHandler(runMethod, jwtSecret) → WebSocketHandler`
58
+ ## 프로토콜 래퍼
22
59
 
23
- 여러 WS 연결 풀 관리. `addSocket(socket, clientId, clientName, req)` 으로 등록(기존 동일 `clientId` 강제 교체). 메시지를 디코드해 `runMethod` 로 RPC, `evt:add/remove/gets/emit`, `auth` 메시지를 처리. `emit(name, infoSelector, data)` 로 매칭 클라이언트 푸시(`ServiceServer.emitEvent` 의 백엔드).
60
+ ### `createServerProtocolWrapper(): ServerProtocolWrapper`
24
61
 
25
- ### `createServiceSocket(socket, clientId, clientName, connReq) ServiceSocket`
62
+ `@simplysm/service-common` `createServiceProtocol()` 기반. 무거운 케이스만 워커 스레드로 위임:
26
63
 
27
- 단일 WS 래퍼. 5초 ping/pong 헬스체크, `createServerProtocolWrapper` 메시지 인코딩, 이벤트 리스너 키 보관, `on("error"|"close"|"message")`.
64
+ - encode: body `Uint8Array` 이거나 `Uint8Array` 요소를 하나라도 가진 배열 워커.
65
+ - decode: 입력 바이트 > 30KB → 워커.
28
66
 
29
- ## 프로토콜
67
+ 워커는 모듈 로드 시 1회 생성 lazy singleton(`maxOldGenerationSizeMb: 4096`). 워커 모듈: `workers/service-protocol.worker.ts`.
30
68
 
31
- ### `createServerProtocolWrapper() ServerProtocolWrapper`
69
+ 표면: `encode(uuid, message)`, `decode(bytes)`, `dispose()`(메인 스레드 프로토콜만 정리, 워커는 공유).
32
70
 
33
- `@simplysm/service-common` 프로토콜을 worker(`service-protocol.worker`) 에 위임할지 메인 스레드에서 처리할지 자동 분기. encode 는 body 에 `Uint8Array` 가 있으면 worker, decode 는 30KB 초과 시 worker. `dispose()` 로 메인 스레드 프로토콜 정리.
71
+ ## 설정 캐시
34
72
 
35
- ## V1 레거시
73
+ ### `getConfig<T>(filePath: string): Promise<T | undefined>`
36
74
 
37
- ### `handleV1Connection(socket, optionsOrMethods, clientNameSetter?)`
75
+ JSON 설정 파일 로더.
38
76
 
39
- `ver` 쿼리가 `"2"` 아닌 클라이언트를 처리. JSON 텍스트 프로토콜로 `{ uuid, command, params, clientName }` 수신. 사용자 `handlers` 가 `handled: true` 를 반환하면 그 결과, 아니면 `SdAutoUpdateService.getLastVersion` fallback, 그것도 아니면 `UPGRADE_REQUIRED` 에러 반환.
77
+ - `LazyGcMap` 캐시: 만료 1시간, GC 10분 간격. 캐시 히트 만료 시간 자동 갱신.
78
+ - 파일 없으면 undefined.
79
+ - `FsWatcher` 로 변경 감시 → 100ms 디바운스 후 리로드. 삭제 감지 시 캐시·워처 정리.
80
+ - 만료 시 워처도 함께 해제.
40
81
 
41
- 타입: `V1Request`, `V1Response`, `V1AutoUpdateMethods`, `V1RequestHandler`, `V1RequestHandlerContext`, `V1RequestHandlerResult`, `V1ConnectionOptions`. `ServiceServerOptions.legacyV1Handlers` `V1RequestHandler[]` 넘기면 `ServiceServer` 자동 사용.
82
+ `ServiceContext.getConfig(section)` root/client `.config.json` 경로를 이 함수로 읽어 `obj.merge`. 경로의 설정 파일을 읽을 때만 직접 호출.
@@ -1,66 +1,57 @@
1
- # @simplysm/service-server — server
1
+ # @simplysm/service-server — 서버
2
2
 
3
- 서버 인스턴스 부트스트랩 표면. `ServiceServer` 가 Fastify · WebSocket · JWT · 정적/업로드/API 라우트를 일괄 등록하고 SIGINT/SIGTERM 정상 종료까지 처리한다.
3
+ ## `createServiceServer<TAuthInfo>(options): ServiceServer<TAuthInfo>`
4
4
 
5
- ## `ServiceServerOptions`
6
-
7
- ```ts
8
- interface ServiceServerOptions {
9
- rootPath: string; // 정적/업로드/설정 루트 (www, .config.json 기준)
10
- port: number;
11
- ssl?: { pfxBytes: Uint8Array; passphrase: string };
12
- auth?: { jwtSecret: string } | false; // undefined: auth 미설정(=auth 서비스 등록 시 에러)
13
- // false: 의도적 비활성화 (검사 스킵)
14
- services: ServiceDefinition[]; // defineService 결과
15
- legacyV1Handlers?: V1RequestHandler[]; // V1 클라이언트 fallback
16
- }
17
- ```
5
+ `new ServiceServer(options)` 단순 팩토리. `TAuthInfo` 는 JWT payload 의 `data` 필드 타입.
18
6
 
19
- ## `createServiceServer<TAuthInfo>(opts) → ServiceServer<TAuthInfo>`
7
+ ## `ServiceServerOptions`
20
8
 
21
- `new ServiceServer(opts)` 단순 래퍼. `TAuthInfo` JWT `data` 페이로드 타입.
9
+ - `rootPath: string` 정적/업로드/설정 루트. 정적은 `<rootPath>/www/`, 업로드 저장은 `<rootPath>/www/uploads/`, 설정은 `<rootPath>/.config.json` `<rootPath>/www/<clientName>/.config.json`.
10
+ - `port: number` — listen 포트. host 는 항상 `0.0.0.0`.
11
+ - `ssl?: { pfxBytes: Uint8Array; passphrase: string }` — 지정 시 HTTPS. 내부에서 `Buffer.from(pfxBytes)` 변환. 미지정 시 helmet 의 `upgrade-insecure-requests` 제거, `hsts`/`crossOriginOpenerPolicy` 비활성.
12
+ - `auth?: { jwtSecret: string } | false`
13
+ - `undefined`(미지정): auth 미구성. 인증 요구 서비스가 하나라도 있으면 `listen()` 시 throw.
14
+ - `false`: 의도적 비활성화. `auth()` 래핑된 서비스/메서드도 인증 스킵.
15
+ - 객체: jwt 시크릿 등록.
16
+ - `services: ServiceDefinition[]` — `defineService()` 결과 배열.
17
+ - `legacyV1Handlers?: V1RequestHandler[]` — ver≠"2" 로 접속한 클라이언트의 커스텀 핸들러. 이 배열도 비고 `AutoUpdate` 서비스도 services 에 없으면 V1 연결을 1008 로 거부.
22
18
 
23
19
  ## `ServiceServer<TAuthInfo>`
24
20
 
25
- ```ts
26
- class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{ ready: void; close: void }> {
27
- readonly fastify: FastifyInstance;
28
- readonly options: ServiceServerOptions;
29
- isOpen: boolean;
21
+ `EventEmitter<{ ready: void; close: void }>` 상속.
30
22
 
31
- listen(): Promise<void>; // 플러그인 등록 + 0.0.0.0 listen + SIGINT/SIGTERM 훅
32
- close(): Promise<void>; // 모든 WS close + fastify.close
33
- signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>; // HS256, 12h
34
- verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>;
35
- getEvent<TEventDef>(eventName): ServerEventProxy<TEventDef>;
36
- emitEvent<TEventDef>(name, infoSelector, data): Promise<void>;
37
- }
38
- ```
23
+ - `readonly options: ServiceServerOptions`
24
+ - `readonly fastify: FastifyInstance` 생성자에서 즉시 생성, 플러그인은 `listen()` 시 등록.
25
+ - `isOpen: boolean`
26
+ - `listen(): Promise<void>` — fastify 플러그인(`@fastify/websocket`, `@fastify/helmet`, `@fastify/multipart`, `@fastify/static`, `@fastify/cors`) 등록, JSON 파서/직렬화기를 `@simplysm/core-common` 의 `json` 으로 교체(Date/BigInt/Uint8Array 보존), 라우트 바인딩, `0.0.0.0:port` listen, SIGINT/SIGTERM 핸들러 등록(10초 후 `process.exit(1)` 강제), `ready` 이벤트 발생.
27
+ - `close(): Promise<void>` — 모든 WebSocket 종료 → `fastify.close()` → `close` 이벤트.
28
+ - `getEvent<TEventDef>(eventName): ServerEventProxy<TEventDef>` — `{ emit(infoSelector, data): Promise<void> }` 핸들 반환.
29
+ - `emitEvent<TEventDef>(eventName, infoSelector, data): Promise<void>` — 클라이언트가 `evt:add` 로 등록한 리스너 중 `infoSelector(info) === true` 인 키에만 푸시.
30
+ - `signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>` — HS256, `exp` 12h 고정. `jwtSecret` 미구성 시 throw.
31
+ - `verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>` — 만료/위조 시 throw.
39
32
 
40
- `listen()` 동작 — 한 번 호출로 모두 등록: helmet/cors/multipart/websocket/@fastify/static, `POST /api/:service/:method` (HTTP RPC), `ALL /upload` (multipart 업로드), `GET /` `GET /ws` (WebSocket: `?ver=2&clientId=&clientName=` 필요. ver 누락 V1 레거시 분기), 와일드카드 `/*` (`<rootPath>/www` 정적 파일).
33
+ `TEventDef` `@simplysm/service-common` `ServiceEventDef`(`{ $info; $data }` 형태). `infoSelector` 클라이언트가 listener 등록보낸 `info` 객체를 받아 boolean 반환.
41
34
 
42
- `auth == null` 인데 `services` 중 하나라도 `authPermissions != null` 이면 `listen()` 에서 즉시 throw.
35
+ ## 자동 등록되는 라우트
43
36
 
44
- 이벤트 브로드캐스트서버는 클라이언트가 `evt:add` 등록한 리스너만 안다. `getEvent(name).emit(infoSelector, data)` 매칭된 리스너에게만 푸시:
37
+ - `ALL /api/:service/:method` RPC. `x-sd-client-name` 헤더 필수, `Authorization: Bearer <jwt>` 옵션. GET `?json=<JSON encoded array>`, POST 는 body 가 params 배열. 그 외 HTTP 메서드 405.
38
+ - `ALL /upload` — multipart 업로드. 인증 헤더 필수. 응답 `ServiceUploadResult[]`.
39
+ - `GET /`, `GET /ws` — WebSocket. 쿼리 `ver=2&clientId=<id>&clientName=<name>` (clientId/clientName 누락 시 1008). ver≠"2" → V1 레거시.
40
+ - `* /*` — 정적 서빙(`/api/...`·`/upload`·`/`·`/ws` 외).
45
41
 
46
- ```ts
47
- server.getEvent<{ $info: { tenantId: string }; $data: { id: string } }>("orderCreated")
48
- .emit((info) => info.tenantId === "T1", { id: "O-100" });
49
- ```
50
-
51
- ## `ServerEventProxy<TEventDef>`
52
-
53
- `getEvent()` 가 반환하는 핸들. `emit(infoSelector, data)` 만 노출.
54
-
55
- ## 최소 예제
42
+ ## 예
56
43
 
57
44
  ```ts
58
- const server = createServiceServer<MyAuth>({
45
+ const server = createServiceServer<{ userId: string }>({
59
46
  rootPath: process.cwd(),
60
47
  port: 50080,
61
- auth: { jwtSecret: env("JWT_SECRET")! },
62
- services: [OrmService, AutoUpdateService, MyService],
48
+ auth: { jwtSecret: process.env.JWT_SECRET! },
49
+ services: [UserService, OrmService],
63
50
  });
64
51
  server.on("ready", () => console.log("up"));
65
52
  await server.listen();
53
+
54
+ const token = await server.signAuthToken({ roles: ["admin"], data: { userId: "u1" } });
55
+ await server.getEvent<{ $info: { shopId: number }; $data: { id: number } }>("order-created")
56
+ .emit(info => info.shopId === 1, { id: 99 });
66
57
  ```
@@ -528,7 +528,9 @@ async onSubmit(): Promise<void> {
528
528
  - 단, **숫자 셀은 `tx-right` 기본 적용** (수량·금액·단가·합계 등 숫자값 컬럼).
529
529
  - `[cell]="items()"` 는 타입 추론용 더미 — 실제 행 데이터는 `<sd-sheet>` 의 `[items]` 가 들고 있다.
530
530
  - 셀 컨텍스트: `let-item="item"` / `let-index="index"` / `let-depth="depth"` / `let-edit="edit"`.
531
- - 셀 안 div 에 배경색 클래스(`bg-theme-*-lightest` 등)를 토글할 때는 빈 값 자리에 `&nbsp;` 등을 채워 div 가 셀 높이를 유지하게 한다. (table cell 자식 div 가 콘텐츠 없을 때 높이 0 → bg 가 셀에 차지 않음.)
531
+ - 셀 안 div 에 배경색 클래스(`bg-theme-*-lightest` 등)를 토글할 때는 빈 값 자리에 `&nbsp;`를 채워 div 가 셀 높이를 유지하게 한다. (table cell 자식 div 가 콘텐츠 없을 때 높이 0 → bg 가 셀에 차지 않음.)
532
+ - 좋은 예: `{{ item.surveyLocationCode ?? "&nbsp" }}`
533
+ - 나쁜 예: `{{ item.surveyLocationCode }}`, `{{ item.surveyLocationCode ?? " " }}`, `{{ item.surveyLocationCode ?? " " }}`
532
534
 
533
535
  **list 안에서**: `<sd-crud-list>` 의 직속 자식으로 `<sd-sheet-column>` 을 두면 내부 시트로 자동 투영된다.
534
536
 
@@ -4,16 +4,16 @@
4
4
 
5
5
  ## 원칙
6
6
 
7
- - 모든 로그는 `consola.withTag(<tag>)` 인스턴스로 출력. `console.*` 직접 호출 금지.
7
+ - 모든 로그는 `createLogger(tag)` (`@simplysm/core-common`) 로 생성한 인스턴스로 출력. `console.*` 직접 호출 금지.
8
8
  - ESLint `no-console` 규칙은 의도된 게이트 — `eslint-disable`/`eslint-disable-next-line no-console` 우회 금지.
9
9
  - 메시지에 `[패키지]` 같은 수동 prefix 금지. tag 가 그 역할.
10
10
 
11
11
  ## 권장 패턴
12
12
 
13
13
  ```ts
14
- import consola from "consola";
14
+ import { createLogger } from "@simplysm/core-common";
15
15
 
16
- const logger = consola.withTag("capacitor:auto-update");
16
+ const logger = createLogger("capacitor:auto-update");
17
17
 
18
18
  // ...
19
19
  logger.info("최신 버전 확인 중");
@@ -40,19 +40,20 @@ const logger = {
40
40
  console.error("[X] 실패:", err);
41
41
  ```
42
42
 
43
- → 모두 `consola.withTag("x")` 1줄로 대체.
43
+ → 모두 `createLogger("x")` 1줄로 대체.
44
44
 
45
45
  ## 환경별 셋업
46
46
 
47
47
  - **Node 진입점(서버·CLI)**: 진입점에서 `setupConsola()` 1회. 자세히 [apis/core-node/consola.md](../apis/core-node/consola.md).
48
48
  - **Browser·Capacitor 진입점**: `setupConsola` 호출 X (Node 전용). consola 기본 reporter 가 브라우저 콘솔로 출력. tag/level/통일된 호출면 충족.
49
49
 
50
- ## 모듈-레벨 logger 주의 (Node 진입점)
50
+ ## 모듈-레벨 logger 주의
51
51
 
52
- Node 진입점에서 `setupConsola` 호출 **전** 에 모듈 레벨에서 `consola.withTag()` 를 호출하면 호출 시점의 옵션(level/reporters) 스냅샷으로 고정되어 이후 setupConsola 변경이 반영되지 않는다.
52
+ 모듈 레벨에서 `consola.withTag()` 를 직접 호출하면 호출 시점의 options(level/reporters) 스냅샷으로 고정되어, 이후 `setupConsola()` reporters 를 갱신해도 child instance 에 반영되지 않는다.
53
53
 
54
- - 해결: `@simplysm/sd-cli` 의 `createLazyLogger(tag)` (`src/runtime/lazy-logger.ts`) 처럼 첫 접근 시점까지 `withTag` 생성을 지연.
55
- - 브라우저·Capacitor setupConsola 없어 문제 없음 — 그냥 모듈 레벨 `consola.withTag()` OK.
54
+ - **해결**: `createLogger(tag)` 사용 (`@simplysm/core-common`, 내부 구현은 lazy Proxy — 메서드 접근 시점까지 `withTag` 생성을 지연).
55
+ - 모든 환경(Node·브라우저·Capacitor)에서 위치(모듈 레벨·함수 내부·class field)에 관계없이 `createLogger` 로 통일.
56
+ - `consola.withTag()` 직접 호출 금지.
56
57
 
57
58
  ## 예외 — `eslint-disable no-console` 가 정당화되는 자리
58
59