Merge pull request 'dev' (#493) from dev into prd-deploy

Reviewed-on: #493
This commit is contained in:
ysCha 2026-05-06 17:51:04 +09:00
commit d5c7966b42
25 changed files with 1081 additions and 23 deletions

7
.claude/memory/MEMORY.md Normal file
View File

@ -0,0 +1,7 @@
- [Project Tech Stack](project_stack.md) — Spring Boot 3.3 + Java 17 + MyBatis + MSSQL, solar panel system (QCAST)
- [Middle server DTOs — act, don't notify](feedback_proactive_middle_server.md) — qcast-api가 중간 서버이므로 통보문만 주지 말고 DTO를 같은 세션에서 바로 수정할 것
- [작업 완료 = 코드+인수인계서+커밋 메시지 한번에](feedback_commit_message.md) — 코드 변경 시 doc/ 인수인계서 동시 갱신, 한국어 커밋 메시지 제시(직접 커밋 X), 해시는 post-commit 훅 자동 기록
- [로컬 애플리케이션 로그 경로](reference_local_log_path.md) — 로컬 실행 시 `logs/api/local.qcast.api.log` (logback FILE appender, local 프로필)
- [커밋 로그 파일 위치](reference_commit_log.md) — qcast-api는 post-commit 훅으로 `memory/commit_log.txt`에 매 커밋 자동 append
- [코드 주석 작성 시 날짜 포함](feedback_comment_date.md) — 파일 생성/수정 시 새로 다는 주석에 항상 작성 날짜(YYYY-MM-DD) 포함
- [Claude 메모리 저장 위치](reference_memory_location.md) — 2026-05-06부터 메모리는 `<repo>/.claude/memory/`에 git 관리, 외부 경로는 symlink. 새 PC는 `.claude/setup-memory-symlink.sh` 실행

View File

@ -0,0 +1,20 @@
---
name: 코드 주석 작성 시 날짜 포함
description: 파일 생성/수정 중 주석을 달 때는 그날의 날짜(YYYY-MM-DD)를 함께 적을 것
type: feedback
---
파일을 생성하거나 수정할 때 **주석을 추가하는 경우, 작성 날짜(YYYY-MM-DD)를 반드시 포함**할 것.
**Why:** 사용자가 변경 이력 추적과 컨텍스트 파악을 쉽게 하기 위함. 주석만 보고도 언제 적힌 것인지 알 수 있어야 git blame을 거치지 않고도 시간 흐름을 파악 가능.
**How to apply:**
- 새로 다는 주석 모두 적용 대상 (한 줄 주석, 블록 주석, JavaDoc, XML 주석 등 형식 무관)
- 형식 예시:
- Java/JS 한 줄: `// 2026-05-06 cha: 발전 시뮬레이션 정렬 로직 추가`
- Java JavaDoc: `* @since 2026-05-06`
- XML/HTML: `<!-- 2026-05-06: FILE appender 활성화 -->`
- SQL: `-- 2026-05-06: 인덱스 힌트 추가`
- 날짜는 항상 시스템 컨텍스트의 `currentDate`(오늘) 기준 절대 날짜로 적을 것 ("어제", "오늘" 같은 상대 표현 금지)
- 기존 주석을 단순히 읽거나 보존하는 경우는 해당 없음 — **새로 작성하는 주석에만** 적용
- 코드 자체에 주석 추가가 불필요한 경우(자명한 코드)에는 주석 자체를 달지 않는 기존 원칙 유지 (이 규칙은 "주석을 달 때"의 형식 규약)

View File

@ -0,0 +1,49 @@
---
name: 작업 완료 시 커밋 메시지 제안 + 인수인계서 자동 갱신 + 커밋 해시는 자동 로그 참조
description: 작업이 끝났을 때 한국어 커밋 메시지 한 줄과 함께 관련 인수인계서까지 자동으로 갱신하고 제시한다. 커밋 해시는 post-commit 훅이 누적하는 commit_log.txt에서 참조한다.
type: feedback
---
## 1. 커밋 메시지 제안
작업이 완료되면 항상 git 커밋에 사용할 메시지를 함께 제시한다.
**Why:** 사용자가 직접 git에 올리며, 매번 커밋 메시지 작성을 요청하는 번거로움을 줄이기 위함. 또한 커밋 본문에 변경 항목 타이틀이 정리돼 있어야 나중에 이력 추적이 쉬움.
**How to apply:**
- 작업 완료 응답 마지막에 별도 섹션으로 커밋 메시지 제시
- 한국어로 **타이틀 한 줄**만 작성 (예: "로컬 프로필 파일 로깅 활성화 + 로그 경로 프로젝트 내부로 이동")
- 사용자는 명령 → 작업 완료 → 즉시 커밋 흐름이므로, 한 작업 단위가 곧 한 커밋. 본문 불릿은 불필요.
- 예외적으로 한 작업에 정말 이질적인 변경이 섞인 경우에만 본문 불릿을 추가 (드문 경우)
- **티켓번호(`[1487]`, `[2133]` 등) prefix는 절대 임의로 붙이지 말 것** — 사용자가 직접 추가하는 영역. Claude는 타이틀 본문만 작성.
- 변경 핵심을 간결하게 (무엇을/어디서/어떻게의 압축형)
- 사용자가 직접 커밋하므로 Claude가 임의로 `git commit` 실행하지 말 것
## 2. 작업 완료 시 인수인계서 동시 갱신 (커밋 신호 기다리지 말 것)
작업이 끝나면 **별도 지시 없어도 관련 인수인계서까지 같은 응답에서 함께 갱신**한다. 사용자는 작업 완료 → 즉시 커밋 흐름이라, "인수인계서도 했지?"라고 한 번 더 물어보게 만들면 안 된다.
**Why:** 사용자 피드백(2026-05-06): "내가 커밋을 하면 자동으로 여기까지 해줘야 한다." 코드 변경만 하고 인수인계서를 빼먹으면 사용자가 커밋 직전에 다시 갱신을 요청해야 하므로 흐름이 끊김. 인수인계서는 후임/리뷰의 1차 자료라 코드 변경과 동시 반영이 필수.
**How to apply:**
- 작업 완료 응답에서: ① 코드 변경 요약 → ② 인수인계서 갱신 → ③ 커밋 메시지 제안 순서로 구성.
- **갱신 대상 인수인계서 매핑** (qcast-api `doc/`):
- 견적서·발전시뮬·Excel/PDF 출력 → `OUTPUT_HANDOVER.md`
- QSP↔프론트 DTO 변경, 중간서버 스키마 → `DTO_MIDDLE_SERVER_HANDOVER.md`
- 인증·로그인 추적·errSave류 → `AUTH_ERROR_HANDOVER.md`
- 그 외 도메인 (BOM/권한/우편번호/배치 등) → `DOMAIN_HANDOVER.md`
- 트랙이 새로 생기면 `HANDOVER_INDEX.md`에도 항목 추가.
- 갱신 방식: **기존 번호 시리즈를 가능한 보존**, 새 항목은 **마지막 번호 다음에 신규 섹션으로 append** (기존 §번호를 한 칸씩 미루지 말 것 — diff 부담↑).
- 새 섹션에는 **작성일(YYYY-MM-DD)**, 배경(무슨 일이 있었나), 한 일(파일·라인), 후속 확인 포인트를 넣음. 사용자 메모리 정책 "코드 주석에 날짜 포함"과 동일 원칙.
- 변경이 사소하거나(오타, 포맷팅 등) 인수인계서 정책에 영향 없는 경우는 갱신 생략 가능. 단 의심되면 갱신 쪽으로 기울 것.
- 인수인계서 갱신 자체도 같은 커밋에 포함되도록 사용자에게 안내 (별도 커밋 분리 옵션도 같이 제시).
## 3. 커밋 해시는 자동 로그에서 참조 (수동 기록 X)
2026-05-06부터 qcast-api 저장소에 git `post-commit` 훅이 설치되어, 커밋 직후 자동으로 `~/.claude/projects/-Users-cha-Works-Projects-qcast3-qcast-api/memory/commit_log.txt`에 한 줄 append 된다.
**How to apply:**
- 인수인계서/리뷰/리포트에 해시를 인용해야 할 때는 그 로그 파일을 읽어 참조 (또는 `git log -1 --format='%h %s'`)
- Claude가 메모리 파일에 해시를 수동으로 누적할 필요 **없음** (이중 기록 금지)
- 푸시 여부는 로그에 기록되지 않으므로, 푸시 확인이 필요하면 `git log origin/<branch>..HEAD` 등으로 별도 확인
- 자세한 위치/형식은 `reference_commit_log.md` 참조

View File

@ -0,0 +1,15 @@
---
name: Middle server DTO updates — act, don't just notify
description: qcast-api is the middle server (QSP API → qcast-api → qcast-front). When QSP or front contract changes, modify qcast-api DTOs directly in the same session
type: feedback
---
아키텍처: **QSP API → qcast-api(중간 서버) → qcast-front**. QSP 응답이나 프론트 요청 스키마가 바뀌면 qcast-api DTO를 통과해야 하므로 DTO 수정이 반드시 필요하다. "통보 템플릿"만 만들고 DTO 수정을 미루지 말 것.
**Why:** qcast-api 자체가 중간 서버다. 외부 통보할 "다른 팀의 서버"가 따로 있지 않다. 사용자가 한 번 물어봤을 때 통보문만 주고 DTO 수정을 미루면, 다음 날 사용자가 "DTO에 추가해야 하는 거 아닌가?"라고 다시 물어 같은 작업을 두 번 하게 된다. 사용자 피드백(2026-04-22): "어제 명령했을 때는 추가 안 하고, 내가 오늘 다시 물으니깐 추가하면 일을 두번 하게 만들어."
**How to apply:**
- QSP 응답 변경 → `dto/` 하위 Response DTO 필드 추가
- 프론트 요청 변경 → `dto/` 하위 Request DTO 필드 추가
- 분석 후 바로 Edit으로 반영, "반영할까요?" 재확인 없이 진행
- 통보 템플릿은 QSP 백엔드 팀 쪽에만 해당 (qcast-api 내부는 직접 수정)

View File

@ -0,0 +1,21 @@
---
name: qcast-api tech stack
description: Spring Boot 3.3 + Java 17 + MyBatis + MSSQL Server project for qcast solar panel system
type: project
---
qcast-api is a Spring Boot 3.3.0 REST API for a solar panel (PV) system called QCAST.
**Tech stack:**
- Java 17, Spring Boot 3.3.0, Maven
- MyBatis 3.0.3 with XML mappers (src/main/resources/mappers/)
- MSSQL Server (database: NEWPVCAD)
- Lombok, Jasypt (password encryption), Spring Security, OpenFeign
- Apache POI / jxls (Excel export), iText html2pdf (PDF generation)
- Springdoc OpenAPI (Swagger UI)
- Spring Batch + Quartz (scheduler)
**Package structure:** com.interplug.qcast.biz.{domain} with Controller/Service/Mapper/DTO pattern
**Why:** Understanding the stack helps provide accurate code suggestions matching existing patterns.
**How to apply:** Use MyBatis XML mapper style for DB queries, follow existing Controller-Service-Mapper layering, write SQL for MSSQL Server syntax.

View File

@ -0,0 +1,23 @@
---
name: 커밋 로그 파일 위치 (post-commit hook 자동 누적)
description: qcast-api 저장소의 .git/hooks/post-commit 훅이 매 커밋 직후 누적 기록하는 plain text 로그 위치와 형식
type: reference
---
qcast-api 저장소는 로컬 git `post-commit` 훅으로 매 커밋 직후 다음 파일에 한 줄을 append 한다.
- **훅이 쓰는 경로**: `~/.claude/projects/-Users-cha-Works-Projects-qcast3-qcast-api/memory/commit_log.txt`
- **실제 저장 위치**: 위 경로는 symlink → `<repo>/.claude/memory/commit_log.txt` 로 해소됨 (2026-05-06부터 메모리를 프로젝트 내부로 이동)
- **gitignore**: `commit_log.txt`는 머신마다 다르므로 git 추적 제외 (`.gitignore``### Claude Code ###` 섹션 참조)
- **형식**: `YYYY-MM-DD HH:MM:SS [브랜치] short(fullhash) 커밋제목`
- **훅 위치**: `<repo>/.git/hooks/post-commit` (qcast-api 로컬 전용, 팀 공유 X)
- **작성일**: 2026-05-06
## 사용 시점
- 사용자가 "최근 커밋 뭐 있었지?", "어제 푸시한 해시 알려줘" 같은 질문을 할 때 이 로그 파일을 먼저 확인한다.
- `git log`와 병행 활용: 로그 파일은 누적 타임스탬프(작업 시각) 기준, `git log`는 커밋 메타데이터 기준이라 둘이 보완적이다.
- 푸시 여부는 기록되지 않음(post-commit만 trigger). push 성공 여부가 필요하면 별도 확인 필요.
## 주의
- 로그 파일이 없을 수도 있음 (아직 커밋이 한 번도 발생하지 않은 경우). 이 경우 `git log`로 대체.
- 다른 저장소에서는 동작하지 않음. 이 hook은 qcast-api 로컬에만 설치됨.

View File

@ -0,0 +1,25 @@
---
name: 로컬 애플리케이션 로그 경로
description: qcast-api 로컬 실행 시 logback이 기록하는 로그 파일 위치 — 디버깅/에러 추적 시 직접 읽을 수 있음
type: reference
---
로컬 프로필(`local`)로 qcast-api 실행 시 로그 파일 위치 (프로젝트 상대경로):
- **현재 로그**: `logs/api/local.qcast.api.log` (절대경로: `/Users/cha/Works/Projects/qcast3/qcast-api/logs/api/local.qcast.api.log`)
- **롤링 백업**: `logs/api/local.qcast.api.YYYY-MM-DD.log` (TimeBasedRollingPolicy, 일자별)
설정 위치: `src/main/resources/logback/logback-local.xml`
- `LOG_HOME=./logs/api/` (프로젝트 작업 디렉터리 기준 상대경로)
- 로그 레벨: `com.interplug.qcast` = DEBUG, root = WARN, Spring Batch/JDBC = DEBUG
- `.gitignore``logs/` 추가됨 (커밋 안 됨)
활용 방법: 사용자가 에러나 동작을 물을 때 로그 파일을 직접 읽어 원인 추적.
- `Read /Users/cha/Works/Projects/qcast3/qcast-api/logs/api/local.qcast.api.log`
- `Bash: tail -n 300 logs/api/local.qcast.api.log`
- `Grep "ERROR\|Exception" logs/api/local.qcast.api.log`
주의:
- 상대경로 `./logs/api/`는 앱 실행 시 working directory 기준. IntelliJ/터미널 모두 보통 프로젝트 루트가 기준이므로 정상 동작.
- 만약 working dir이 달라지면 다른 위치에 생성될 수 있음 — 파일이 없으면 사용자에게 실행 위치 확인 요청.
- dev/prd 프로필은 다른 경로 사용 (이 메모리는 로컬 전용)

View File

@ -0,0 +1,28 @@
---
name: Claude 메모리 저장 위치 (프로젝트 내부)
description: qcast-api의 Claude 자동 메모리 파일이 저장되는 실제 위치와 다른 PC에서 셋업하는 방법
type: reference
---
2026-05-06부터 qcast-api의 Claude 자동 메모리는 **프로젝트 내부**에 저장하고 git으로 관리한다.
## 구조
- **실제 저장 위치**: `<repo>/.claude/memory/` (git 추적 대상)
- **Claude Code가 접근하는 경로**: `~/.claude/projects/-Users-cha-Works-Projects-qcast3-qcast-api/memory/`
- 위 경로는 `<repo>/.claude/memory/` 로 향하는 **symlink**
- **commit_log.txt** 만 예외적으로 머신 로컬 (gitignore 처리됨)
## 다른 PC에서 처음 셋업할 때
저장소 클론 직후 한 번만 실행:
```sh
sh .claude/setup-memory-symlink.sh
```
스크립트는 멱등성이 보장되며, Claude Code 인코딩 규칙(working dir 절대경로의 `/``-`로 치환)에 맞춰
`~/.claude/projects/<encoded>/memory` symlink를 자동 생성한다. 클론 위치가 다르면 인코딩도 자동으로 달라진다.
## 활용
- 메모리 파일을 추가/수정하면 git diff에 잡히므로 다른 변경과 함께 커밋 가능
- 새 메모리 작성 시에도 기존 자동 메모리 흐름 그대로 (`Write``MEMORY.md` 인덱스 추가) — 어느 경로로 쓰든 symlink를 통해 같은 파일을 가리킴
- `MEMORY.md`도 git에 함께 커밋되어 다른 PC에서 즉시 동일한 인덱스 사용 가능

14
.claude/settings.json Normal file
View File

@ -0,0 +1,14 @@
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "DEBUG_FILE=/tmp/claude-debug.log; LOG_FILE=/logs/api/local.qcast.api.log; if [ -f \"$DEBUG_FILE\" ] && [ -s \"$DEBUG_FILE\" ]; then echo '=== [ClaudeDebug 덤프] ==='; cat \"$DEBUG_FILE\"; echo '=== [덤프 END] ==='; fi; if [ -f \"$LOG_FILE\" ]; then ERRORS=$(tail -n 200 \"$LOG_FILE\" | grep -E 'ERROR|WARN|Exception' | tail -n 20); if [ -n \"$ERRORS\" ]; then echo '=== [APP 에러/경고 로그] ==='; echo \"$ERRORS\"; echo '=== [LOG END] ==='; fi; fi"
}
]
}
]
}
}

49
.claude/setup-memory-symlink.sh Executable file
View File

@ -0,0 +1,49 @@
#!/bin/sh
# 2026-05-06: Claude Code auto-memory 시스템이 사용하는 외부 디렉터리(~/.claude/projects/<encoded-path>/memory)를
# 이 저장소의 .claude/memory 로 향하는 symlink로 만들어 주는 스크립트.
#
# 배경:
# - Claude Code는 working directory 경로를 인코딩(슬래시→하이픈)해서 ~/.claude/projects/<encoded>/memory 에
# 메모리 파일을 읽고 쓴다. 이 경로는 머신/체크아웃 위치마다 달라진다.
# - 메모리 .md 파일들은 git에 커밋되어 있고, 다른 PC에서는 symlink만 새로 걸어 주면
# 기존 메모리를 그대로 활용할 수 있다.
#
# 사용:
# sh .claude/setup-memory-symlink.sh
#
# 멱등성: 이미 올바른 symlink가 존재하면 아무 동작도 하지 않는다.
set -eu
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MEMORY_SRC="$REPO_ROOT/.claude/memory"
# Claude Code 인코딩 규칙: 절대경로 슬래시(/)를 하이픈(-)으로 치환한 디렉터리 이름
ENCODED="$(printf '%s' "$REPO_ROOT" | sed 's|/|-|g')"
TARGET_DIR="$HOME/.claude/projects/$ENCODED"
TARGET_LINK="$TARGET_DIR/memory"
if [ ! -d "$MEMORY_SRC" ]; then
echo "[setup-memory-symlink] ERROR: $MEMORY_SRC 가 없습니다. 먼저 git pull 하세요."
exit 1
fi
mkdir -p "$TARGET_DIR"
# 이미 같은 곳을 가리키는 symlink면 종료
if [ -L "$TARGET_LINK" ]; then
CURRENT="$(readlink "$TARGET_LINK")"
if [ "$CURRENT" = "$MEMORY_SRC" ]; then
echo "[setup-memory-symlink] OK: 이미 연결됨 ($TARGET_LINK -> $MEMORY_SRC)"
exit 0
fi
echo "[setup-memory-symlink] 기존 symlink 갱신: $CURRENT -> $MEMORY_SRC"
rm "$TARGET_LINK"
elif [ -e "$TARGET_LINK" ]; then
echo "[setup-memory-symlink] ERROR: $TARGET_LINK 가 일반 파일/디렉터리로 존재합니다."
echo " 내용 백업 후 직접 정리하세요. (이 스크립트는 안전을 위해 강제 삭제하지 않습니다.)"
exit 1
fi
ln -s "$MEMORY_SRC" "$TARGET_LINK"
echo "[setup-memory-symlink] 연결 완료: $TARGET_LINK -> $MEMORY_SRC"

6
.gitignore vendored
View File

@ -37,3 +37,9 @@ build/
### logs ###
qcast3/
src/test
logs/
### Claude Code ###
# 2026-05-06: 머신 로컬 전용 파일 — settings.local.json, post-commit 훅 누적 로그
.claude/settings.local.json
.claude/memory/commit_log.txt

110
doc/AUTH_ERROR_HANDOVER.md Normal file
View File

@ -0,0 +1,110 @@
# 인증 / 로그인 추적 / 에러 저장 인수인계서
> 작성일: 2026-05-06
> 대상: 후임 AI 엔지니어 또는 개발 관리자
> 작업 브랜치: `dev_ysCha`
> **이 문서의 범위**: 로그인·로그아웃 추적, 클라이언트 에러 저장 API 트랙만 다룹니다.
> 다른 트랙은 `HANDOVER_INDEX.md` 참조.
---
## 0. 한 줄 요약
운영 중 **자동 로그인/로그아웃이 의도대로 안 되는 케이스**와 **클라이언트에서 발생한 예외를 잡지 못하는 문제**를 추적하기 위해, 추적 로그와 에러 저장 API를 단계적으로 보강한 트랙입니다.
---
## 1. 로그인 / 로그아웃 추적
### 1.1 한 일
| 단계 | 내용 | 커밋 |
|------|------|------|
| 1차 | 로그인·로그아웃 추적 | `60b84191` |
| 2차 | 자동 로그인 추적 | `1bf777f8` |
### 1.2 추적 로그의 목적
- 어디서 토큰이 사라지는지
- 어디서 자동 로그인 트리거가 안 잡히는지
- 어디서 로그아웃이 누락되는지
> 프론트(qcast-front) 에도 같은 목적의 추적 트랙이 있습니다 (`API_STABILITY_HANDOVER.md` 5절). 프론트와 백엔드 양쪽 로그를 **같은 시점 기준으로 매칭**해서 봐야 원인이 보입니다.
### 1.3 주의
- 추적 로그는 디버깅 목적으로 추가된 것이라, **운영에서 떼어낼지 유지할지** 사용자에게 확인이 필요합니다.
- 토큰/세션 정보를 로그에 그대로 남기면 보안 이슈가 생기므로, 로그 내용에 민감 정보가 안 들어가는지 다시 확인하세요.
---
## 2. 에러 저장 API — `errSave` / `quotationErrSave`
### 2.1 한 일
| 단계 | 내용 | 커밋 |
|------|------|------|
| 1차 | `errSave` API 추가 | `63969fa2` |
| 2차 | `errSave` API 추가 (보강) | `23f70b21` |
| 3차 | `errSave` API 추가 2 | `d22a7508` |
| 별건 | `quotationErrSave` API 추가 (견적서 전용) | `0b9eeb99` |
### 2.2 무엇을 하나
- 클라이언트(프론트)에서 발생한 예외 / 에러를 **백엔드로 전송해 저장**.
- 운영 중 사용자가 알아채지 못하는 에러 추적용.
### 2.3 사용처 (프론트 측)
- 글로벌 에러 핸들러
- 주요 fetch 실패 catch 블록
- 견적서 출력 실패는 `quotationErrSave` 로 분리
### 2.4 주의
- 에러 저장 자체가 또 실패하면 **무한 루프** 가 될 수 있습니다. 저장 실패 시에는 추가 호출을 하지 않도록 막아야 합니다.
- 로그에 사용자 입력값을 통째로 담으면 PII 가 들어갈 수 있어 위험. 필요한 식별자만 남기세요.
---
## 3. 안정성 보강
### 3.1 `itemList` null 체크
- 커밋: `b84e1fd0`
- 견적서 항목 리스트가 null 인 경우 NPE 가 나던 문제. 호출부에서 null 체크 추가.
### 3.2 `HttpMediaTypeNotAcceptableException` 에러
- 커밋: `50d8a9e3`
- 특정 요청에서 Accept 헤더 불일치로 예외 발생 → 컨버터/Producer 설정 보강.
---
## 4. 핵심 파일
| 파일/위치 | 역할 |
|----------|------|
| `com.interplug.qcast.biz.auth.*` | 로그인/세션 관련 |
| `com.interplug.qcast.biz.{도메인}.*ErrSaveController` 또는 Service | errSave 엔드포인트 |
| `com.interplug.qcast.biz.quotation.*ErrSave*` | quotationErrSave 엔드포인트 |
| 글로벌 ExceptionHandler | 알 수 없는 예외 처리 |
> 정확한 클래스명은 변경되었을 수 있으니 IDE 검색으로 `errSave` 키워드 찾는 게 빠릅니다.
---
## 5. 인수 직후 권장 절차
1. 이 문서 통독.
2. `errSave` 키워드로 코드 베이스 검색해서 엔드포인트 위치 확인.
3. 로그인 컨트롤러에서 토큰 발급/검증 흐름 한 번 따라가 보기.
4. 운영 로그에서 `자동 로그인` / `로그아웃` 키워드로 grep 해 어떤 패턴이 잡히는지 확인.
---
## 6. 미해결 / 추적 중
- **자동 로그인 실패 케이스 완전 재현 안 됨**. 추적 로그를 보면 단서가 보이는데, 환경별로 다르게 나타남.
- 인증 추적 로그를 **운영에서 유지/제거**할지 사용자 결정 필요.
끝.

153
doc/DOMAIN_HANDOVER.md Normal file
View File

@ -0,0 +1,153 @@
# 도메인 기능 인수인계서 (BOM / 권한 / API / 우편번호 / 배치)
> 작성일: 2026-05-06
> 대상: 후임 AI 엔지니어 또는 개발 관리자
> 작업 브랜치: `dev_ysCha`
> **이 문서의 범위**: 출력·DTO·인증 외 나머지 도메인 작업.
> 다른 트랙은 `HANDOVER_INDEX.md` 참조.
---
## 0. 한 줄 요약
[1xxx] 티켓 시리즈와 새로 추가된 API들이 모이는 곳. **BOM 부모/자식 의미**, **오브젝트 복사 권한**, **발전시뮬레이션 API 신규 개발**, **우편번호 하이픈 자동 처리**, **배치(Quartz) 포트/시간 조정** 등 다양한 도메인 작업이 들어 있습니다.
---
## 1. BOM (자재 명세서)
### 1.1 `[1487]` 자식 BOM 조회 쿼리 컬럼 의미 수정
- 커밋: `0c979df6`
- 가장 헷갈렸던 부분: **컬럼명과 의미가 직관과 반대**.
- `PACKAGE_ITEM_ID` = **자식**
- `ITEM_ID` = **부모**
- BOM 조회/저장 쿼리를 새로 손댈 때 항상 이 의미를 다시 확인하세요. **반대로 알면 부모-자식 관계가 통째로 뒤집힙니다.**
### 1.2 권장
- 새 매퍼 쿼리에 컬럼 alias를 줄 때 `PARENT_ITEM_ID` / `CHILD_ITEM_ID` 같이 의미 명시한 별칭을 써서 후임자가 헷갈리지 않게 하세요.
---
## 2. 권한
### 2.1 `[1485]` 오브젝트 복사 기능 권한 설정
- 커밋: `491f9304`, `31e3c77a`
- 오브젝트 복사 액션은 모두에게 열려 있지 않고 **권한이 있는 사용자만** 수행 가능하게 제한.
- 권한 체크는 Controller/Service 레이어에서. 프론트에서도 버튼이 노출/비노출되는지 양쪽 정합성 확인 필요.
---
## 3. 발전시뮬레이션 관련 신규 API
### 3.1 `[?]` 발전시뮬레이션 관련 견적서 조회 API
- 커밋: `7211101a`
- 발전시뮬레이션이 참조하는 견적서를 조회하는 신규 엔드포인트.
### 3.2 `[?]` 발전시뮬레이션 물건번호 목록 조회 API
- 커밋: `c7253388`
- 물건번호(objectNo) 목록을 발전시뮬레이션 화면에 노출.
### 3.3 `[1516]` 발전량 수정 — 중복 모듈 처리
- 커밋: `3b090f7e`
- 같은 모듈이 중복으로 잡혀 발전량 계산이 중복되던 문제 수정.
### 3.4 `[1375]` 인증용량 쿼리 수정
- 커밋: `2c0a8a35`, `0a0e248b`
- 인증용량 산출 쿼리 보정. (수정이 두 번 나뉜 걸 보면 한 번에 안 잡힘 — 새 케이스가 발견되면 또 분기 추가될 가능성.)
---
## 4. 모듈 / PCS / 거치대
### 4.1 `[1479]` 물건 정보 화면에 일부 PCS 명칭이 표시되지 않는다 — 스토리지뱃더리 추가
- 커밋: `3a4dab2f`
- "스토리지뱃더리"(스토리지 배터리, 일본어 표기)를 PCS 종류에 추가.
### 4.2 `[1763]` 특정 지붕재 사용 시 Plan 복사 오류
- 커밋: `1b4e64b5`
- 특정 지붕재가 들어간 Plan을 복사하면 지붕재 정보가 올바르게 전달되지 않음 → 복사 시 매핑 보정.
### 4.3 `[1764]` 북쪽 면 설치 사양 변경
- 커밋: `408cc75a`
- 북쪽 면 설치 사양 처리 분기 추가.
### 4.4 `[1423]` 설계 가져오기 기능
- 커밋: `2b43fad2`
- 다른 설계를 현재 화면으로 가져오는 기능 보강.
---
## 5. 우편번호
### 5.1 한 일
| 내용 | 커밋 |
|------|------|
| 판매점 우편번호 등록 시 하이픈이 없으면 **셋째 자리 뒤에 자동 하이픈 조합**으로 등록 처리 | `e05ef305` |
| 우편번호 변경 부분 추가 | `4e502c42` |
### 5.2 정책
- 일본 우편번호는 `XXX-XXXX` 7자리 + 하이픈 형식. 사용자가 하이픈 없이 7자리만 넣어도 받아주도록 자동 보정.
---
## 6. 배치 (Spring Batch + Quartz)
### 6.1 한 일
| 내용 | 커밋 |
|------|------|
| 배치 잡이 8080, 8081 두 곳에서 실행되던 것을 8080만 실행하도록 수정 | `711c8fcd` |
| 개발에 `batch: scheduler-port: 8090` 추가 | `4bcc2fe9` |
| `materialJob`: `02:30 → 04:50`, `bomJob`: `02:40 → 05:20` 시간 조정 | `19250727` |
| job 수정 / 제목 한 줄에 나타내기 | `6b072e42`, `4759c572` |
### 6.2 주의
- 배치 잡 시간을 옮길 때는 **다른 잡과 겹치지 않는지** 반드시 확인하세요. 같은 시간에 여러 잡이 돌면 DB 부하/락이 발생합니다.
- 잡이 두 인스턴스(8080/8081)에서 동시에 도는 사고가 있었던 영역이라 **포트 분리 정책**이 들어가 있습니다.
---
## 7. Java 버전 / 빌드 환경
### 7.1 한 일
- 커밋 `73da60a0`, `9b762063`, `d68b1270`, `037e3201` (java 버전 맞춤 1~3)
- 로컬·CI 자바 버전 불일치로 빌드 실패가 반복돼 **Java 17** 로 정렬.
### 7.2 주의
- 새 환경 세팅 시 `JAVA_HOME` 이 17 인지 먼저 확인.
- `pom.xml``<java.version>` 도 17 로 고정.
---
## 8. 핵심 파일 / 위치
| 위치 | 역할 |
|------|------|
| `com.interplug.qcast.biz.bom.*` | BOM 도메인 |
| `com.interplug.qcast.biz.simulation.*` | 발전시뮬레이션 |
| `com.interplug.qcast.biz.permission.*` 또는 Controller 단의 권한 어노테이션 | 권한 |
| `com.interplug.qcast.batch.*` | 배치 잡 / Quartz 설정 |
| `application*.yml` | 포트/스케줄러 포트/잡 시간 등 설정 |
---
## 9. 인수 직후 권장 절차
1. 이 문서 통독.
2. `application.yml``application-{profile}.yml` 차이를 확인 (특히 배치 포트).
3. BOM 자식 조회 쿼리 한 번 직접 실행 → 부모/자식 컬럼 의미 체득.
4. 발전시뮬레이션 신규 API 두 개의 endpoint URL을 Swagger UI에서 확인.
---
## 10. 미해결 / 추적 중
- `[1375]` 인증용량 쿼리는 **케이스가 더 있을 수 있음** (이미 두 차례 수정됨).
- 배치 잡 시간 정책은 운영 데이터 양에 따라 또 옮길 가능성 있음.
- 권한 체크가 Controller/Service 두 레이어에 산재되어 있어 **표준화 여지**가 있음 (시간 되면).
끝.

View File

@ -0,0 +1,114 @@
# 중간 서버 DTO 변경 인수인계서
> 작성일: 2026-05-06
> 대상: 후임 AI 엔지니어 또는 개발 관리자
> 작업 브랜치: `dev_ysCha`
> **이 문서의 범위**: qcast-api가 중간 서버로서 통과시키는 DTO 변경 작업만 다룹니다.
> 다른 트랙은 `HANDOVER_INDEX.md` 참조.
---
## 0. 한 줄 요약
`QSP API → qcast-api → qcast-front` 의 가운데 다리 역할이라, **양쪽 스키마가 바뀌면 중간 DTO를 반드시 통과시켜야** 합니다. "외부에 통보하면 끝"이 아니라 **여기서 직접 코드를 고칩니다**.
---
## 1. 가장 중요한 원칙 — 통보 X, 직접 수정 O
### 1.1 무엇을 잘못하기 쉬운가
QSP 응답이나 프론트 요청 스키마가 바뀌었다는 얘기를 들었을 때, AI가 자주 하는 실수:
> "QSP 응답에 X 필드가 추가되었습니다. DTO에 추가가 필요합니다." (← 이렇게 끝내고 코드는 안 고침)
이 프로젝트에서는 **틀린 행동**입니다.
### 1.2 사용자 피드백 (2026-04-22)
> "어제 명령했을 때는 추가 안 하고, 내가 오늘 다시 물으니깐 추가하면 일을 두번 하게 만들어."
### 1.3 올바른 행동
- 분석 → **그 자리에서 Edit으로 DTO 필드 추가**
- "반영할까요?" 재확인 없이 진행
- 통보 템플릿은 QSP 백엔드 팀 쪽에만 해당 (qcast-api 내부는 직접 수정)
---
## 2. 작업 위치
### 2.1 DTO 폴더
| 종류 | 위치 |
|------|------|
| Request DTO (프론트 → qcast-api) | `com.interplug.qcast.biz.{도메인}.dto.*Request` |
| Response DTO (QSP → qcast-api → 프론트) | `com.interplug.qcast.biz.{도메인}.dto.*Response` |
> 도메인별로 패키지가 다르므로 IDE의 "Find in Files" 로 필드명 검색이 빠릅니다.
### 2.2 수정 시 점검 포인트
1. **Request 인 경우**: 프론트가 보내는 필드를 받을 수 있는지 (필드명/타입 일치).
2. **Response 인 경우**: QSP 응답을 그대로 통과시키는지 (또는 변환 로직이 있는지).
3. **Lombok `@Data` 또는 `@Getter/@Setter`** 가 붙어 있는지 확인.
4. **Swagger 문서**에도 자동 반영되는지(어노테이션 사용 여부).
---
## 3. 한 일 (대표 사례)
### 3.1 PCS 접속함 `moduleTpCd`
- 커밋: `475733d2`
- PCS 접속함 **응답/요청 DTO 모두**에 `moduleTpCd` 필드 추가.
- 양방향이므로 두 군데를 동시에 수정해야 함.
### 3.2 `trestles[].subModuleTpCd`
- 커밋: `45f8bbd4`
- `trestles[]` 배열 요소에 `subModuleTpCd` 추가.
- **Cross Mix 식별용**. 단일/동계열은 빈 값.
### 3.3 `addresseeCompanyName` 50자 제한
- 커밋: `b2305295`
- 수신자 회사명 50자 제한. (DTO/검증/저장 어느 단에서 자르는지 확인하고 일관되게)
### 3.4 `selectParentAgentInfo` 관련
- 커밋: `fd2703ba`
- 부모 에이전트 정보 조회 — DTO + Mapper + Service 한 세트.
### 3.5 `CATEGORY` 20자 제한
- 커밋: `4db2ddfd`
- `CATEGORY, LEN(CATEGORY) AS M_USER` 20자 제한.
---
## 4. 검증 / 길이 제한 정책
길이 제한이 들어가는 필드들은 보통 다음 위치 **모두**에 같은 값을 둬야 합니다.
| 위치 | 역할 |
|------|------|
| DTO 필드 | `@Size(max = N)` 등으로 검증 |
| Mapper XML | DB 컬럼 길이 일치 |
| 프론트 입력 폼 | 입력 단에서 자르기 / 안내 |
> 한 곳만 50자, 다른 곳 100자 이렇게 어긋나면 운영에서 끊어집니다. 변경 시 전부 같이.
---
## 5. 인수 직후 권장 절차
1. 이 문서 통독.
2. `dto/` 하위 패키지 한 번 훑어 어떤 도메인이 있는지 확인.
3. 가장 최근 DTO 변경 커밋(`475733d2`, `45f8bbd4`) 의 diff 한 번 따라가 보기.
4. QSP 응답 ↔ 프론트 요청 매핑 흐름을 견적서 도메인 하나로 따라가 보기.
---
## 6. 미해결 / 추적 중
- 중간 서버 특성상 **상시 변경 영역**입니다. QSP 또는 프론트 변경 보고가 들어오면 즉시 DTO부터 확인하세요.
- 현재까지 별도의 미해결 DTO 이슈는 없으나, **변경 누락 = 운영 장애** 직결이므로 보수적으로.
끝.

122
doc/HANDOVER_INDEX.md Normal file
View File

@ -0,0 +1,122 @@
# 인수인계 문서 인덱스 (qcast-api)
> 작성일: 2026-05-06
> 대상: 후임 AI 엔지니어 또는 개발 관리자
> 작업 브랜치: `dev_ysCha`
---
## 0. 이 폴더는 뭔가요?
**qcast-api** 프로젝트(태양광 시스템 QCAST의 백엔드 API)의 **트랙별 인수인계 문서**가 모여 있습니다. 한 파일에 다 넣으면 너무 길고 영역이 섞여서 보기 어렵기 때문에 4개 트랙으로 분리했습니다.
**읽는 순서**: 위에서부터 차례로. 출력(Excel/PDF) 트랙이 가장 양이 많고 손댈 일도 많으니 먼저 읽으세요.
> **참고**: 이 프로젝트는 **중간 서버**입니다. 구조는 `QSP API → qcast-api → qcast-front` 입니다. "다른 팀 서버에 알려야 한다" 가 아니라 **여기서 직접 DTO/로직을 고치면 됩니다**.
---
## 1. 트랙별 문서
### [OUTPUT_HANDOVER.md](./OUTPUT_HANDOVER.md) ★ 가장 자주 손댐
**견적서 / 발전시뮬레이션 / Excel·PDF 출력 트랙.**
견적서·중량·구조도·계통도 등의 Excel/PDF 출력 관련 작업이 가장 많은 영역입니다. 정렬·파일명·이미지·전각문자·도면 비율 등 출력물에서 보이는 거의 모든 것이 여기에 모입니다.
핵심 키워드: 견적서 / 발전시뮬레이션 / Excel(jxls+POI) / PDF(iText html2pdf) / 정렬 / 파일명 포맷 / 전각문자 / 도면 비율 / ®→Ⓡ→(R) / 폰트
---
### [DTO_MIDDLE_SERVER_HANDOVER.md](./DTO_MIDDLE_SERVER_HANDOVER.md)
**중간 서버 DTO 변경 트랙.**
QSP API 응답 또는 프론트 요청 스키마가 바뀌면 qcast-api DTO를 통과해야 하므로 DTO 수정이 필수입니다. **통보문만 주고 미루면 다음 날 같은 일을 또 하게 됩니다.**
핵심 키워드: 중간 서버 / DTO 수정 / moduleTpCd / subModuleTpCd / Cross Mix / addresseeCompanyName 50자 / selectParentAgentInfo
---
### [AUTH_ERROR_HANDOVER.md](./AUTH_ERROR_HANDOVER.md)
**인증 / 로그인 추적 / 에러 저장 API 트랙.**
자동 로그인·로그아웃 동작 추적, 클라이언트 에러를 백엔드에 저장하는 `errSave`/`quotationErrSave` API.
핵심 키워드: 로그인·로그아웃 추적 / 자동 로그인 추적 / errSave / quotationErrSave / itemList null 체크
---
### [DOMAIN_HANDOVER.md](./DOMAIN_HANDOVER.md)
**도메인 기능 트랙 (BOM / 권한 / 발전시뮬레이션 API / 우편번호 / 배치 등).**
출력·DTO·인증 외 나머지 도메인 작업 전반. [1xxx] 티켓 시리즈와 새로 추가된 API들.
핵심 키워드: BOM 부모/자식 / 오브젝트 복사 권한 / 발전시뮬레이션 물건번호 API / 우편번호 하이픈 / 배치(Batch) / Quartz / 인증용량 쿼리 / [1xxx] 티켓
---
## 2. 같이 봐야 하는 자료
### 메모리 인덱스
- 위치: `~/.claude/projects/-Users-cha-Works-Projects-qcast3-qcast-api/memory/MEMORY.md`
- 작업 시 지켜야 할 사용자(Cha) 작업 스타일이 들어 있습니다.
- **작업 시작 전에 반드시 통독하세요.**
### 커밋 로그 (자동 누적, 2026-05-06~)
- 위치: `~/.claude/projects/-Users-cha-Works-Projects-qcast3-qcast-api/memory/commit_log.txt`
- 본 저장소의 `.git/hooks/post-commit` 훅이 매 커밋 직후 한 줄 append 합니다.
- 형식: `YYYY-MM-DD HH:MM:SS [브랜치] short(fullhash) 커밋제목`
- 인수인계서/리뷰에 해시를 인용해야 할 때 우선 참조. 푸시 여부는 기록되지 않으므로 별도 확인.
### 사내 자료 (참고)
- 자바 코드 스타일: `doc/eclipse-java-google-style.xml`
- 자매 프로젝트 인수인계: `qcast-front/docs/HANDOVER_INDEX.md` (프론트 영역의 결정 사항이 백엔드 동작 이해에도 필요할 때 참조)
---
## 3. 인수받은 직후 추천 절차
1. **이 인덱스 통독** (5분).
2. **메모리 `MEMORY.md` 통독** — 사용자 스타일 / 중간 서버 원칙 / 커밋 메시지 규칙 (5~10분).
3. **`OUTPUT_HANDOVER.md` 통독** — 가장 자주 손대는 영역 (20~30분).
4. 나머지 3개 트랙 문서는 **필요할 때 펼쳐 보는 레퍼런스** 로 사용.
5. `pom.xml` / `application.yml` / 매퍼 폴더 구조를 한 번 훑어 패키지 구성을 머리에 넣기.
---
## 4. 핵심 원칙 3줄 요약
이 프로젝트에서 일을 한다면 적어도 이 3개는 머리에 두세요.
1. **중간 서버다.** QSP/프론트 스키마가 바뀌었다는 정보를 받으면, 통보만 하지 말고 **그 자리에서 DTO를 직접 수정**.
2. **출력물(Excel/PDF)은 양쪽을 따로 본다.** Excel은 jxls/POI, PDF는 iText html2pdf — 같은 견적서라도 두 곳을 동시에 고쳐야 함.
3. **작업 끝나면 한국어 한 줄 커밋 메시지를 함께 제시**한다. (단, **AI가 직접 `git commit` 하지 말 것**, 사용자가 직접 커밋함.)
---
## 5. 기술 스택 한눈에
| 항목 | 내용 |
|------|------|
| 언어 | Java 17 |
| 프레임워크 | Spring Boot 3.3.0, Maven |
| DB 접근 | MyBatis 3.0.3 (XML 매퍼: `src/main/resources/mappers/`) |
| 데이터베이스 | MSSQL Server (DB명: `NEWPVCAD`) |
| 보안/암호화 | Spring Security, Jasypt |
| 외부 API 호출 | OpenFeign |
| Excel | Apache POI / jxls |
| PDF | iText html2pdf |
| API 문서 | Springdoc OpenAPI (Swagger UI) |
| 스케줄러 | Spring Batch + Quartz |
**패키지 구조**: `com.interplug.qcast.biz.{도메인}` 안에 `Controller / Service / Mapper / DTO`.
---
## 6. 브랜치 전략
- `main` — 운영 (PR 대상은 보통 여기)
- `dev` — 개발 통합
- `dev-deploy` — 개발 서버 배포용
- `dev_ysCha` — 담당자 개인 작업 브랜치 (현재 체크아웃)
- `hotfix/9999` — 긴급 수정용
- `feature/{이름이나 번호}` — 기능별 브랜치
---
끝.

230
doc/OUTPUT_HANDOVER.md Normal file
View File

@ -0,0 +1,230 @@
# 견적서 / 발전시뮬레이션 / Excel·PDF 출력 인수인계서
> 작성일: 2026-05-06
> 대상: 후임 AI 엔지니어 또는 개발 관리자
> 작업 브랜치: `dev_ysCha`
> **이 문서의 범위**: Excel/PDF 출력물 관련 작업만 다룹니다.
> 다른 트랙은 `HANDOVER_INDEX.md` 참조.
---
## 0. 한 줄 요약
견적서·중량 시트·구조도·계통도 등 **출력물에서 보이는 거의 모든 이슈**가 이 트랙으로 들어옵니다. 정렬·파일명·이미지·전각문자·도면 비율·폰트 등이 반복적으로 나오는 영역이며, **Excel(jxls + POI)****PDF(iText html2pdf)** 두 출력 경로를 동시에 봐야 합니다.
---
## 1. 출력 시스템 개요
### 1.1 두 가지 출력 경로
| 출력 종류 | 사용 라이브러리 | 템플릿 위치 |
|----------|----------------|-------------|
| **Excel** | Apache POI + jxls | `src/main/resources/template/excel/` |
| **PDF** | iText html2pdf | (HTML 템플릿 → PDF 변환) |
> **중요**: 같은 견적서라도 Excel과 PDF는 **별도 경로**입니다. 한쪽만 고치면 다른 쪽이 어긋납니다. "정렬을 왼쪽으로" 같은 표시 변경은 양쪽을 함께 손대야 합니다.
### 1.2 자주 나오는 출력 종류
- 견적서 (Quotation)
- 발전시뮬레이션 결과
- 중량 계산 시트
- 배치도 / 계통도 / 구조도
---
## 2. 정렬 (Alignment) 변경 작업
### 2.1 한 일
| 티켓 | 내용 | 커밋 |
|------|------|------|
| `[1508]` | 견적서 [품명][형번] 가운데 → **왼쪽 정렬** (Excel + PDF 둘 다) | `11d2acb2` |
| `[2133]` | 중량 계산 시트 [품명][모델명] **왼쪽 정렬** (Excel) | `1c22880c` |
### 2.2 주의
- 정렬은 사용자 가독성에 직접 영향. 변경 전 항상 **현 상태 스크린샷**을 떠 두세요.
- Excel만 바꾸고 PDF를 안 바꿔서 보고가 들어오는 경우가 잦습니다.
---
## 3. 파일명 / 다운로드명 포맷
### 3.1 한 일
| 티켓 | 내용 | 커밋 |
|------|------|------|
| `[2133]` | 다운로드 파일명 포맷에 `planNo` 추가 → `objectNo_planNo_날짜` (견적서/발전시뮬레이션 통일) | `fc6655f9` |
### 3.2 정책
- 파일명 포맷은 견적서·발전시뮬레이션이 **동일 규칙**으로 가야 함 (사용자 요청).
- 새 다운로드를 추가할 때도 같은 패턴을 따르세요.
---
## 4. 전각문자 / unknown 처리
### 4.1 한 일
| 티켓 | 내용 | 커밋 |
|------|------|------|
| `[1827]` | 엑셀/PDF 다운로드 시 파일명이 `unknown` 으로 나오는 문제 — **전각문자 처리** | `dd2b3e4d` |
### 4.2 배경
- 일본어 전각문자(예: ``)가 파일명/HTTP 헤더에 들어가면 인코딩 문제로 `unknown` 으로 떨어집니다.
- 파일명 인코딩 단계에서 전각 → 반각 변환 또는 URL 인코딩 강화로 처리.
### 4.3 일반화 가능한 교훈
- **이 시스템은 일본어 환경**입니다. 새 출력에서 파일명을 만들 때 항상 전각문자 가능성을 의심하세요.
---
## 5. 등록상표 ® 처리 (카카오 시리즈)
### 5.1 한 일
PDF 출력 시 등록상표 기호 `®``?` 로 깨지는 문제. 단계별로 변경:
| 단계 | 내용 | 커밋 |
|------|------|------|
| 1차 | `®``Ⓡ` 로 치환 | `47f18b50` |
| 2차 | `®``(R)` + PDF 폰트 변경 | `57d52c7c` |
| 3차 | PDF 폰트 복구 | `33555619` |
### 5.2 교훈
- PDF 폰트는 **글리프 지원 범위**를 항상 확인. 안 되는 글리프는 텍스트 치환으로 우회하는 게 안전.
- 폰트를 바꾸면 다른 글자(특히 한자/일본어)가 깨지는 부작용이 생기므로, **폰트 변경은 최후의 수단**.
---
## 6. 도면 비율 / 화질 개선
### 6.1 한 일
| 티켓 | 내용 | 커밋 |
|------|------|------|
| `[1427]` | 출력한 견적서 도면의 가로/세로 비율이 화면과 다른 문제 (+ 로컬 실행 가능 조정) | `3048076d` |
| `[1470]` | 배치도·계통도·구조도 화질 개선 (cell 이미지 추가) | `4468d3f9`, `1a4becd1`, `512cdd5c` 등 다수 |
| `[1268][1295]` | PDF 출력 세로 → 가로 혼합 | `abf1f91a`, `3612c50d` |
### 6.2 주의
- 화질 개선 작업은 같은 티켓에 여러 커밋이 따라옵니다(이미지 교체 반복). 한 번에 안 되는 게 정상.
- 도면 비율은 **DPI / 픽셀 단위 / mm 단위** 가 섞여 있어 한 곳만 고치면 다른 출력이 어긋납니다.
---
## 7. 견적서 주소 (가장 험난했던 시리즈) — `[1379]`
### 7.1 무슨 일이 있었나
견적서 주소 표시 조건을 바꿨다가 → 원복 → 다시 수정 → 또 원복 → 재수정 의 반복이 있었습니다.
| 단계 | 커밋 |
|------|------|
| 견적서 주소 수정 | `a430ce64` |
| 견적서 출력 쿼리 수정 | `c28eeee2` |
| 1차 원복 | `1fcd2dac` |
| 2차 원복 | `32e6237f` |
| 3/9 이전 원복 | `35584228` |
| 최초 원복 | `c735263d` |
| 최초 원시 원복 | `150b64b0` |
| 재수정 | `b5e3012d` |
### 7.2 경고
- **이 영역은 손대기 전에 반드시 사용자에게 현재 정책을 재확인**하세요.
- 견적서 주소는 일본 거래처별로 표시 요건이 다를 수 있어 케이스가 많습니다.
---
## 8. 지붕/사 각도 출력 관련
| 티켓 | 내용 | 커밋 |
|------|------|------|
| `[1765]` | 출력한 견적서 지붕 면이 이중 표시 | `9dce727f`, `dc7eb154` |
| `[1480]` | 사(斜)의 각도 설정을 "각도"로 했을 때 출력물 문제 | `709b1f1f`, `f407079f`, `3e64d8a2` |
지붕·각도 관련 출력은 프론트(qcast-front)의 SK(Straight Skeleton) 트랙과 맞물립니다. 프론트의 `SK_MOVEMENT_HANDOVER.md` 도 같이 보세요.
---
## 9. 핵심 파일 / 위치
| 위치 | 역할 |
|------|------|
| `src/main/resources/template/excel/` | Excel 템플릿(jxls) |
| `src/main/resources/mappers/` | 출력에 쓰이는 SQL 매퍼 |
| `com.interplug.qcast.biz.quotation.*` | 견적서 도메인 (Controller/Service/Mapper/DTO) |
| `com.interplug.qcast.biz.simulation.*` | 발전시뮬레이션 도메인 |
| (PDF 관련) Service/Util 클래스 | iText html2pdf 변환 로직 |
> 파일 경로는 패키지 변경 가능성이 있으므로 항상 현재 트리에서 다시 확인하세요.
---
## 10. 인수 직후 권장 절차
1. 이 문서 통독.
2. `template/excel/` 폴더 안 템플릿 한두 개 열어 jxls 태그 사용법 확인.
3. 견적서 도메인 Controller → Service → Mapper → 템플릿 한 번 따라가 보기.
4. 로컬에서 견적서 Excel/PDF 한 번 출력해보고 결과 확인.
---
## 11. 미해결 / 추적 중
- `[1379]` 견적서 주소 정책은 **여전히 변동 가능성** 있음. 추가 요청 들어오면 신중히.
- 화질 개선(`[1470]`) 은 일단락됐지만 같은 종류의 이미지 깨짐 보고가 또 들어올 수 있음.
---
## 12. 旧見積書書式(EXCEL2) 출력 시트 정책 — 2026-05-06
### 12.1 무슨 일이 있었나
일본에서 다음 요청이 옴.
> 旧見積書書式 선택 시에도 [図面/シミュレーションファイルを含む] 옵션이면 `割付図・系統図`/`架台図`/`発電シミュレーション` 시트를, [架台重量表付き] 옵션이면 `重量算出シート` 시트를 함께 출력해 달라. 결국 **신(통상) 견적서와 동일한 시트 출력 사양**으로 통일 요구.
기존 코드(`EstimateService.java` `excelDownload()``EXCEL2` 분기)는 旧見積書書式 선택 시 **schDrawingFlg/schWeightFlg 옵션과 무관하게** 부속 시트를 모두 강제 제거하고 `見積書.` 한 장만 남기고 있었음.
### 12.2 한 일
`EstimateService.java` 1915~1925 (수정 후) 영역을 다음 정책으로 단순화.
- `schDrawingFlg`/`schWeightFlg`로 시트를 골라내는 1단계 분기는 그대로 유지 (= 신 양식과 동일하게 작동).
- 양식 분기는 **견적서 시트만 교체** 책임으로 축소:
- `EXCEL2` (旧書式) → `見積書` 제거 + `見積書.`을 첫 번째 시트로 이동
- 그 외 (신 양식) → `見積書.` 제거
- `特異事項` 시트는 두 양식 모두 항상 제거.
결과적으로 旧書式 출력 순서:
```
見積書. → 発電シミュレーション → 割付図・系統図 → 架台図 → 重量算出シート → P1 → P2
```
(옵션에 따라 일부 시트 빠짐)
### 12.3 견적서 시트 두 종류 차이 (혼동 주의)
| 항목 | `見積書` (신/통상) | `見積書.` (구) |
|---|---|---|
| 템플릿 sheet 번호 | sheet1.xml | sheet7.xml |
| 영역 | A1:AP46 (가로) | A1:V104 (세로 길쭉) |
| 병합셀 | 61 | 343 |
| 아이템 출력 | 인덱스 고정 (`list[0]~list[26]`) | JXLS each 반복 (`item.itemName`) |
→ "이름이 거의 같으니 같은 시트 아니냐"라는 인상과 달리 **레이아웃·바인딩 키가 완전히 다른 별개 양식**. 한쪽 손볼 때 다른 쪽도 자동 적용된다고 착각하지 말 것.
### 12.4 후속 확인 포인트 (다음 작업 시 체크)
1. **프론트(qcast-front)** 가 旧書式 선택 시 `schDrawingFlg`/`schWeightFlg` 값을 함께 전송하는지 확인. 안 보내면 백엔드만 고쳐도 효과 없음.
2. `EstimateRequest.schWeightFlg` 필드는 정의되어 있으나 분기에서는 `schDrawingFlg.indexOf("4")`만 보고 있음. 프론트가 별도 키로 보내는 경우 백엔드 분기 조건도 함께 손봐야 함.
3. 旧書式 템플릿(`excel_download_quotation_detail_template2.xlsx`) 시트 구성은 신 양식 템플릿(`excel_download_quotation_detail_template.xlsx`)과 동일. 旧書式 전용으로 부속 시트 디자인을 다르게 가져갈 일이 생기면 템플릿 자체를 분리해야 함.
### 12.5 활성 시트
`setSheetOrder("見積書.", 0)`로 옮겨도 POI는 active sheet 인덱스를 자동 보정하지 않을 수 있음. 열었을 때 다른 시트가 먼저 보이면 `workbook.setActiveSheet(0)` 한 줄을 추가.
끝.

View File

@ -1912,19 +1912,20 @@ public class EstimateService {
safeRemoveSheet(workbook, "特異事項");
}
// 2026-05-06 / 견적서 양식에 따른 견적서 시트만 교체.
// schDrawingFlg / schWeightFlg(위쪽 분기) 선택된 부속 시트
// (割付図系統図 / 架台図 / 発電シミュレーション / 重量算出シート ) 그대로 유지한다.
if ("EXCEL2".equals(estimateRequest.getSchDownload())) {
// 旧見積書書式: 양식 견적서 시트(見積書) 제거 , 양식(見積書.) 앞으로 이동
safeRemoveSheet(workbook, "見積書");
safeRemoveSheet(workbook, "特異事項");
safeRemoveSheet(workbook, "発電シミュレーション");
safeRemoveSheet(workbook, "割付図・系統図");
safeRemoveSheet(workbook, "架台図");
safeRemoveSheet(workbook, "重量算出シート");
safeRemoveSheet(workbook, "P1");
safeRemoveSheet(workbook, "P2");
}else{
safeRemoveSheet(workbook,"見積書.");
safeRemoveSheet(workbook, "特異事項");
if (workbook.getSheetIndex("見積書.") > 0) {
workbook.setSheetOrder("見積書.", 0);
}
} else {
// (통상) 견적서 양식: 양식 견적서 시트(見積書.) 제거
safeRemoveSheet(workbook, "見積書.");
}
safeRemoveSheet(workbook, "特異事項");
applyExcelNumberFormat(workbook);
// applyExcelFixedNumberFormat(workbook, Arrays.asList("H25", "C12"), "#,##0.00");
@ -3181,6 +3182,10 @@ public class EstimateService {
SimulationEstimateListResponse response = new SimulationEstimateListResponse();
if (!"T01".equals(objectRequest.getSaleStoreId())) {
objectRequest.setSchSelSaleStoreId("");
}
// 견적서 목록 조회
List<ObjectResponse> objectList = estimateMapper.selectSimulationEstimateList(objectRequest);

View File

@ -34,14 +34,22 @@ public class ExcelDownController {
@ResponseStatus(HttpStatus.OK)
public QuotResponse quotExclDownData(@RequestBody QuotRequest quotRequest) throws Exception {
List<QuotPlanResponse> quotPlanExclDownData =
excelDownService.selectQuotPlanExclDownData(quotRequest);
List<QuotItemResponse> quotItemExclDownData =
excelDownService.selectQuotItemExclDownData(quotRequest);
QuotResponse quotRes = new QuotResponse();
quotRes.setQuotPlanList(quotPlanExclDownData);
quotRes.setQuotItemList(quotItemExclDownData);
if ("1".equals(quotRequest.getSch_selectType())) {
List<QuotPlanResponse> quotPlanExclDownData =
excelDownService.selectQuotPlanExclDownData(quotRequest);
List<QuotItemResponse> quotItemExclDownData =
excelDownService.selectQuotItemExclDownData(quotRequest);
quotRes.setQuotPlanList(quotPlanExclDownData);
quotRes.setQuotItemList(quotItemExclDownData);
} else {
List<QuotPlanResponse> quotPlanExclDownData =
excelDownService.selectQuotPlanExclDownData2(quotRequest);
quotRes.setQuotPlanList(quotPlanExclDownData);
}
return quotRes;
}

View File

@ -23,6 +23,15 @@ interface ExcelDownMapper {
*/
List<QuotPlanResponse> selectQuotPlanExclDownData(QuotRequest quotRequest) throws Exception;
/**
* 과거데이터_견적 엑셀다운로드 조회 - 심플모드
*
* @param quotRequest
* @return
* @throws Exception
*/
List<QuotPlanResponse> selectQuotPlanExclDownData2(QuotRequest quotRequest) throws Exception;
/**
* 과거데이터_견적 엑셀다운로드 조회(아이템)
*

View File

@ -38,6 +38,18 @@ public class ExcelDownService {
return excelDownMapper.selectQuotPlanExclDownData(quotRequest);
}
/**
* 과거데이터_견적 엑셀다운로드 조회
*
* @param quotRequest
* @return
* @throws Exception
*/
public List<QuotPlanResponse> selectQuotPlanExclDownData2(QuotRequest quotRequest)
throws Exception {
return excelDownMapper.selectQuotPlanExclDownData2(quotRequest);
}
/**
* 과거데이터_견적 엑셀다운로드 조회(아이템)
*

View File

@ -117,5 +117,8 @@ public class QuotPlanResponse {
private String saleOther;
@Schema(description = "가대판매가격 架台販売価格")
private String saleStand;
@Schema(description = "플랜정보 등록일시 プラン情報登録日時")
private String createDatetime;
@Schema(description = "플랜정보 등록자 プラン情報登録者")
private String createUser;
}

View File

@ -11,6 +11,8 @@ import lombok.Setter;
public class QuotRequest {
@Schema(description = "Language Code")
private String langCd;
@Schema(description = "조회타입")
private String sch_selectType;
@Schema(description = "시작일")
private String sch_startDt;
@Schema(description = "종료일")

View File

@ -3,7 +3,7 @@
xsi:noNamespaceSchemaLocation="http://www.padual.com/java/logback.xsd">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="ACTIVE_PROFILE" source="spring.profiles.active"/>
<property name="LOG_HOME" value="/logs/api/"/>
<property name="LOG_HOME" value="./logs/api/"/>
<property name="LOG_FILE" value="${LOG_HOME}${ACTIVE_PROFILE}.qcast.api"/>
<property name="LOG_FILE_EXT" value=".log"/>
@ -33,7 +33,7 @@
<root level="warn">
<appender-ref ref="CONSOLE"/>
<!-- <appender-ref ref="FILE" />-->
<appender-ref ref="FILE" />
</root>
<!-- Spring Batch 로깅 레벨 설정 -->

View File

@ -1612,9 +1612,6 @@
<if test='schObjectNo != null and schObjectNo != ""'>
AND O.OBJECT_NO LIKE '%' + #{schObjectNo} + '%'
</if>
<if test='schSaleStoreId != null and schSaleStoreId != ""'>
AND O.SALE_STORE_ID = #{schSaleStoreId}
</if>
<if test='schOtherSelSaleStoreId != null and schOtherSelSaleStoreId != ""'>
/* 2차점까지 고름 */
AND O.SALE_STORE_ID = #{schOtherSelSaleStoreId}

View File

@ -192,6 +192,42 @@
</if>
</select>
<select id="selectQuotPlanExclDownData2" parameterType="com.interplug.qcast.biz.excelDown.dto.QuotRequest"
resultType="com.interplug.qcast.biz.excelDown.dto.QuotPlanResponse">
/* sqlid : com.interplug.qcast.api.excelDown.selectQuotPlanExclDownData2 (견적엑셀다운로드 플랜정보 데이터 조회) */
SELECT
O.OBJECT_NO
, P.PLAN_NO
, P.LAST_EDIT_DATETIME
, P.LAST_EDIT_USER
, PI.CREATE_DATETIME
, PI.CREATE_USER
FROM T_OBJECT O WITH (NOLOCK)
LEFT OUTER JOIN T_OBJECT_INFO OI WITH (NOLOCK)
ON O.OBJECT_NO = OI.OBJECT_NO
LEFT OUTER JOIN T_PLAN P WITH (NOLOCK)
ON O.OBJECT_NO = P.OBJECT_NO
LEFT OUTER JOIN T_PLAN_INFO PI WITH (NOLOCK)
ON P.OBJECT_NO = PI.OBJECT_NO
AND P.PLAN_NO = PI.PLAN_NO
LEFT OUTER JOIN M_SALES_STORE SS
ON O.SALE_STORE_ID = SS.SALE_STORE_ID
WHERE P.DEL_FLG = '0'
AND (O.DEL_FLG = '0' OR (OI.SOURCE_ORIGIN = 'QCAST_III' AND OI.ORG_DEL_FLG = '0' AND OI.TEMP_FLG = '0'))
<if test="sch_startDt != null and sch_startDt != '' and sch_endDt != null and sch_endDt != ''"> <!-- 견적일 -->
AND (
(P.LAST_EDIT_DATETIME <![CDATA[>=]]> #{sch_startDt} + ' 00:00:00' AND P.LAST_EDIT_DATETIME <![CDATA[<=]]> #{sch_endDt} + ' 23:59:59') OR
(PI.CREATE_DATETIME <![CDATA[>=]]> #{sch_startDt} + ' 00:00:00' AND PI.CREATE_DATETIME <![CDATA[<=]]> #{sch_endDt} + ' 23:59:59')
)
</if>
<if test="sch_saleStoreId != null and sch_saleStoreId != ''"> <!-- 판매대리점명/ID -->
AND O.SALE_STORE_ID = #{sch_saleStoreId}
</if>
<if test="sch_businessChargerCd != null and sch_businessChargerCd != ''"> <!-- 영업담당자 -->
AND SS.BUSINESS_CHARGER_CD = #{sch_businessChargerCd}
</if>
</select>
<select id="selectQuotItemExclDownData" parameterType="com.interplug.qcast.biz.excelDown.dto.QuotRequest"
resultType="com.interplug.qcast.biz.excelDown.dto.QuotItemResponse">
/* sqlid : com.interplug.qcast.api.excelDown.selectQuotItemExclDownData (견적엑셀다운로드 품목단위 데이터 조회) */