# Work Server

`Fastify + Knex + PostgreSQL` 기반의 범용 작업용 API 서버입니다.

## 추천 DB

- `PostgreSQL`
- 이유:
  - Node 생태계에서 검증된 조합
  - `Knex`로 CRUD와 DDL을 함께 다루기 편함
  - 운영/확장/마이그레이션 측면에서 무난함

## 실행

```bash
docker compose up -d
docker compose logs -f work-server
```

`work-server`는 `3100` 포트를 점유하는 nginx 프록시이고, 실제 API 런타임은 `work-server-blue` / `work-server-green` 슬롯으로 동작합니다. 재기동은 비활성 슬롯을 먼저 새로 빌드해 `/health` 확인 후 프록시 업스트림을 전환하고, 마지막에 이전 슬롯을 내리는 방식으로 무중단 전환합니다.

운영 기본 규칙:

- `work-server` 재기동은 기존 활성 슬롯을 바로 내리는 단일 컨테이너 재시작으로 처리하지 않습니다.
- 항상 `비활성 슬롯 기동 -> /health 확인 -> nginx upstream 전환 -> 이전 슬롯 정리` 순서를 유지합니다.
- 문서, 스크립트, 운영 가이드에 재기동 예시를 추가할 때도 무중단 전환 절차를 기본값으로 적고, 연결이 끊기는 재시작은 장애 대응이나 예외 상황으로만 취급합니다.

슬롯 로그까지 같이 보려면 아래처럼 확인합니다.

```bash
docker compose logs -f work-server work-server-blue work-server-green
```

호스트 프로젝트 루트와 동일한 문맥으로 서버 재기동을 처리하려면 별도 host runner를 사용합니다. 이 runner는 별도 명시적 요청이 있을 때만 수동으로 켜거나 재기동합니다.

```bash
cd /home/how2ice/project/ai-code-app
npm run server-command:runner
```

## 환경 변수

기본 실행은 `.env.example` 값으로도 가능합니다.
로컬 환경에 맞는 값을 덮어쓰려면 `.env.example`를 참고해서 `.env`를 추가하면 됩니다.

주요 항목:

- `APP_TIME_ZONE`: Node 서버 런타임 기준 시간대. 기본값 `Asia/Seoul`
- `DB_TIME_ZONE`: 앱이 여는 DB 세션 시간대. 기본값 `Asia/Seoul`
- `DB_*`: PostgreSQL 접속 정보
- `PLAN_WORKER_ENABLED`: Plan 자동화 worker 활성화 여부
- `PLAN_WORKER_INTERVAL_MS`: Plan polling 주기
- `PLAN_GIT_REPO_PATH`: 브랜치 생성/병합 대상 저장소 경로
- `PLAN_MAIN_PROJECT_REPO_PATH`: main 반영 후 pull 받을 메인 루트 프로젝트 경로. 비우면 `PLAN_GIT_REPO_PATH`를 사용
- `PLAN_RELEASE_BRANCH`: 자동 merge 대상 release 브랜치명
- `IOS_NOTIFICATION_ENABLED`: iOS APNs 알림 활성화 여부
- `APNS_*`: Apple Push Notification 인증 키 정보
- `SERVER_COMMAND_DOCKER_SOCKET`: 서버 재기동 명령이 사용할 Docker Unix socket 경로. rootless Docker면 예: `/run/user/1000/docker.sock`
- `SERVER_COMMAND_API_BASE_URL`: `work-server`가 서버 재기동 요청을 위임할 host runner 주소
- `SERVER_COMMAND_API_ACCESS_TOKEN`: host runner 호출 토큰
- `SERVER_COMMAND_TEST_CHECK_URL`, `SERVER_COMMAND_REL_CHECK_URL`, `SERVER_COMMAND_PROD_CHECK_URL`: 외부 공개 URL과 별개로 재기동 성공 판정에 사용할 내부 확인 URL. 비워 두면 각 `SERVER_COMMAND_*_URL` 값을 그대로 사용합니다.

서버 재기동 기능을 쓰려면 `work-server` 컨테이너가 Docker에 접근할 수 있어야 합니다. 기본값은 `/var/run/docker.sock`이며, rootless Docker 환경이면 `.env`에 `SERVER_COMMAND_DOCKER_SOCKET` 또는 `DOCKER_HOST=unix:///run/user/<uid>/docker.sock`를 맞춰 준 뒤 `work-server`를 다시 올려야 합니다.

기본 예시는 `http://host.docker.internal:3211/api`로 맞춰져 있어서, `work-server` 컨테이너가 아니라 호스트의 현재 프로젝트 루트에서 `restart-*.sh`를 실행합니다. 즉 `Server > Command`가 직접 CLI로 재기동한 것과 최대한 비슷한 문맥을 사용합니다.

## Codex Live

`Codex Live`는 현재 프로젝트 환경에서 `PLAN_MAIN_PROJECT_REPO_PATH`로 지정된 `main_project` 저장소 루트를 기준으로 실행됩니다. 문서 예시와 기본 컨테이너 값은 `PLAN_MAIN_PROJECT_REPO_PATH=/workspace/main-project`이지만, 이것은 환경 변수 기본 예시일 뿐이며 실제 실행 루트는 현재 마운트된 프로젝트 경로를 따릅니다.

소스 수정이 필요하면 **현재 실행 환경에서 실제로 연결된 `main_project` 저장소 루트의 `AGENTS.md` 규칙을 먼저 확인한 뒤**, 그 작업 트리에서 바로 수정합니다.

현재 운영 기준에서는 `Codex Live`, 일반 채팅, 일반 작업메모 반영 요청은 **현재 프로젝트 루트의 로컬 작업본을 직접 기준으로 처리**합니다.

`Codex Live`와 `Plan` 자동화는 별개입니다. `Codex Live`는 채팅 유형 context를 최우선 기준으로 읽고, 채팅방 공통 문맥과 전용 메모는 충돌하지 않는 범위의 보조 문맥으로만 사용합니다. 현재 화면 및 최근 대화 문맥도 그 아래 보조 문맥으로 사용합니다. 자동화 유형 context는 기본 문맥으로 섞지 않습니다.

브라우저 기준 화면 테스트, 소스 변경 검증, 최종 화면 확인은 **`https://preview.sm-home.cloud/`** 기준으로 진행합니다. `https://test.sm-home.cloud/`는 운영 비교나 프록시 점검이 꼭 필요할 때만 보조적으로 확인합니다. 별도 요청이 없는 한 `sm-home.cloud`나 `rel.sm-home.cloud`는 기본 검증 대상으로 삼지 않습니다.

채팅에서 파일, 문서, 이미지, 코드 같은 리소스를 제공할 때의 기본 공개 경로는 `public/.codex_chat/<chat-session-id>/resource/...`입니다. Codex가 원본 파일 경로만 답해도 서버가 이 위치로 세션 전용 사본을 만들고, 채팅에는 공개 URL을 다시 적어 줍니다.

채팅 첨부 파일도 같은 기준을 사용하며 `public/.codex_chat/<chat-session-id>/resource/uploads/...` 아래에 저장됩니다.

## Plan 자동화

`Plan` 게시판 항목을 작업 큐처럼 읽어 자동화할 수 있습니다.

현재 운영 기준에서 자동화 작업메모의 세부 Git 절차는 이 문서에 고정하지 않습니다. 상태 전이와 실제 처리 흐름은 worker 구현, 환경 변수, 현재 운영 정책을 함께 확인해야 합니다.

자동화 실행기는 선택된 자동화 유형의 description/context만 우선 참조합니다. `Codex Live`나 일반 채팅 문맥은 자동화 기본 context로 사용하지 않습니다.

안전 조건:

- Git worktree가 깨끗해야 동작
- `release` 브랜치가 실제로 존재해야 병합 가능
- 실패 시 자동으로 `이슈` 상태와 오류 메시지를 남김

## 주요 API

- `GET /health`
- `GET /api/schema/tables`
- `POST /api/ddl/create-table`
- `POST /api/ddl/drop-table`
- `POST /api/ddl/add-column`
- `POST /api/ddl/drop-column`
- `POST /api/ddl/raw`
- `POST /api/crud/:table/select`
- `POST /api/crud/:table/insert`
- `PATCH /api/crud/:table/update`
- `DELETE /api/crud/:table/delete`
- `GET /api/plan/statuses`
- `POST /api/plan/setup`
- `GET /api/plan/items`
- `GET /api/plan/items/:id`
- `POST /api/plan/items`
- `PATCH /api/plan/items/:id`
- `DELETE /api/plan/items/:id`
- `POST /api/notifications/setup`
- `GET /api/notifications/tokens`
- `GET /api/notifications/subscriptions/web`
- `PUT /api/notifications/tokens/ios`
- `DELETE /api/notifications/tokens/ios`
- `POST /api/notifications/send-test`

## iOS 알림 연동

- 프론트에서 알림 `On` 시 `PUT /api/notifications/tokens/ios`로 APNs 토큰을 등록합니다.
- 프론트에서 알림 `Off` 시 또는 토큰이 폐기되면 `DELETE /api/notifications/tokens/ios`로 토큰을 제거합니다.
- Plan worker가 브랜치 준비, 자동 작업 완료/실패, release 반영 완료/실패, main 반영 완료/실패 시 등록된 iOS 토큰으로 APNs 알림을 전송합니다.

## 웹푸쉬 호출 메모

- `POST /api/notifications/send`는 `title`, `body`, `data`, `threadId` 외에 `targetDeviceIds`도 받을 수 있습니다.
- `targetDeviceIds`를 넣으면 `web_push_subscriptions.device_id` 또는 PWA iOS 토큰의 `device_id`가 일치하는 기기에만 알림을 보냅니다.
- 기존 `targetClientIds`도 호환 입력으로는 허용되지만, 새 호출은 `targetDeviceIds` 사용을 기준으로 합니다.
- 웹푸시 구독과 PWA iOS 토큰 등록 시 서버는 `appOrigin`, `appDomain`도 함께 저장합니다.
- `POST /api/notifications/send`에 `targetAppOrigins`, `targetAppDomains`를 넣으면 해당 앱 도메인/오리진으로 등록된 구독에만 발송할 수 있습니다.
- `GET /api/notifications/subscriptions/web`로 현재 저장된 웹푸시 구독의 `deviceId`, `appOrigin`, `appDomain`, `enabled` 상태를 확인할 수 있습니다.
- 같은 알림을 교체하려면 DB 삭제 대신 `data.notificationKey` 또는 `threadId`를 고정값으로 보내세요. 서비스워커가 이 값을 브라우저 알림 `tag`로 사용해 이전 알림을 대체합니다.
