Compare commits

...

14 Commits

Author SHA1 Message Date
ttipo 67f5d815c9 DB 서비스 이름을 postgres에서 db로 변경
Deploy / deploy (push) Successful in 20s
- depends_on, DB_HOST 기본값도 db로 갱신
- deploy: 서비스 이름 변경으로 남는 orphan 컨테이너 정리를 위해 --remove-orphans 추가

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:15:38 +09:00
ttipo f3e97d6c44 DB 데이터 경로를 프로젝트 폴더 기준 상대 경로로 변경
Deploy / deploy (push) Successful in 15s
- DB_DATA_DIR 기본값 /data/postgres -> ./data/postgres (다른 스택과 동일하게 프로젝트 폴더 하위에 생성)
- data/ gitignore 추가
- deploy: backend/frontend를 매 배포마다 force-recreate - DB 교체 시 스키마 미동기화, nginx 낡은 upstream 캐시 문제 방지

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:02:38 +09:00
ttipo 126c0169aa DB 볼륨을 /data/postgres 바인드 마운트로 변경
Deploy / deploy (push) Successful in 35s
- named volume 대신 호스트 경로 바인드 마운트 사용 (DB_DATA_DIR, 기본 /data/postgres)
- backend: DB 연결/동기화 실패 시 프로세스 종료 - restart: always가 DB 준비까지 재시도하도록 fail-fast 적용

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:56:44 +09:00
ttipo 8770b1b47b 호스트 포트 매핑 제거, npm 프록시 경유 구조로 전환
Deploy / deploy (push) Successful in 19s
- 모든 서비스 ports -> expose로 변경 (외부 포트 노출 없음)
- frontend/backend를 공용 proxy 네트워크에 연결 - npm이 컨테이너 이름(report-frontend:80)으로 라우팅
- DB는 내부 app-network 전용 유지
- 미사용이 된 BACKEND_PORT/FRONTEND_PORT 변수 제거

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:43:52 +09:00
ttipo dbcda9dd6b compose 네트워크 이름을 app-network로 통일
Deploy / deploy (push) Successful in 13s
다른 스택(adform, innertab)과 동일한 명명 규칙 적용

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:41:28 +09:00
ttipo 1d21d4b49b 비밀값 하드코딩 기본값 제거
Deploy / deploy (push) Successful in 37s
- docker-compose: DB_PASSWORD, ACCESS_SECRET, REFRESH_SECRET, ADMIN_USER, ADMIN_PASS를 필수(:?)로 변경 - 누락 시 명확한 에러로 실패
- server.js: 시크릿/관리자 계정의 하드코딩 폴백 제거 (JWT 시크릿 누락 시 기동 중단, 관리자 계정 미설정 시 생성 건너뜀)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:02:42 +09:00
ttipo c8dbaf167f Revert "frontend를 npm 공용 proxy 네트워크에 연결"
Deploy / deploy (push) Successful in 13s
This reverts commit 0e9e5b0283.
2026-07-19 15:42:10 +09:00
ttipo 0e9e5b0283 frontend를 npm 공용 proxy 네트워크에 연결
Deploy / deploy (push) Successful in 20s
npm(Nginx Proxy Manager)이 proxy 네트워크에서 report-frontend:80 으로 접근할 수 있도록 external 네트워크 추가 (adform/innertab과 동일 패턴). 도메인 접속 시 502 나던 문제 해결

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:38:48 +09:00
ttipo 539fb8aa61 frontend: 로컬 vite 실행 경로 제거, Docker 단일 환경으로 정리
Deploy / deploy (push) Successful in 38s
- dev/preview 스크립트 제거 (Docker 빌드는 build만 사용)
- .env.example 안내를 .env.production 기준으로 갱신, .env.development 삭제

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:27:16 +09:00
ttipo b118a58b8f backend: 로컬 npm 실행 경로 제거, Docker 단일 환경으로 정리
- dev 스크립트(nodemon)와 dotenv 의존성/로드 코드 제거 (설정은 compose 환경변수로만 주입)
- backend/.env.example 삭제 - 환경변수 템플릿은 루트 .env.example로 일원화

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:25:38 +09:00
ttipo 32b5a8ed0e frontend: 미사용 PASSWORD_ADMIN 환경변수 제거
frontend 소스에서 참조하지 않는 죽은 설정 정리 (백엔드 슈퍼 패스워드 제거 후속)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:18:00 +09:00
ttipo d7e31ae38c deploy: 저장소 이름 변경(blog_report -> report) 반영
Deploy / deploy (push) Failing after 1s
저장소가 report로 이름이 바뀌면서 배포 워크플로우의 git 주소와 DEPLOY_DIR이 옛 이름을 가리켜 Pull 단계에서 즉시 실패하던 문제 수정

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:08:27 +09:00
ttipo 5f7fa3689f 로그인 슈퍼 패스워드 백도어 제거
Deploy / deploy (push) Failing after 0s
하드코딩된 관리자 슈퍼 패스워드로 모든 계정에 로그인할 수 있던 로직을 제거하고, bcrypt 본인 비밀번호 검증만 사용

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:59:49 +09:00
ttipo a45cb39a57 DB를 MySQL에서 PostgreSQL로 전면 전환
- Sequelize dialect를 postgres로 변경, mysql2 대신 pg/pg-hstore 사용
- docker-compose: postgres:16-alpine + pg_isready healthcheck, 호스트 포트 5442
- 환경변수 MYSQL_PORT -> DB_PORT로 통일, .env.example 갱신

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:59:37 +09:00
11 changed files with 237 additions and 228 deletions
+5 -7
View File
@@ -4,12 +4,14 @@
# 이 파일을 복사해서 .env 로 만들고 값을 채워 넣으세요.
# ============================================================
# MySQL
MYSQL_PORT=3306
DB_HOST=mysql
# PostgreSQL
DB_PORT=5432
DB_HOST=db
DB_USER=
DB_PASSWORD=
DB_NAME=report_db
# DB 데이터 저장 경로 (프로젝트 폴더 기준 상대 경로, 기본값 ./data/postgres)
DB_DATA_DIR=./data/postgres
# Admin Account
ADMIN_USER=
@@ -21,7 +23,3 @@ REFRESH_SECRET=
# Gemini API Key
JEMINI_KEY=
# Ports
BACKEND_PORT=54530
FRONTEND_PORT=55110
+7 -4
View File
@@ -4,7 +4,7 @@
# 사전 준비 (배포 서버에서 1회):
# 1. act_runner를 host 모드 라벨 "deploy"로 등록 (러너 계정: deploy)
# 2. 영구 배포 디렉터리 준비:
# git clone https://git.wandergo.net/partnership/blog_report.git ~/stacks/_clients/blog_report
# git clone https://git.wandergo.net/partnership/report.git ~/stacks/_clients/report
# 해당 디렉터리에 .env 와 frontend/.env.production 을 생성 (chmod 600)
#
# .env 는 gitignore 대상이라 git 작업에 영향받지 않고 서버에 유지됩니다.
@@ -16,7 +16,7 @@ on:
branches: [main]
env:
DEPLOY_DIR: /home/deploy/stacks/_clients/blog_report
DEPLOY_DIR: /home/deploy/stacks/_clients/report
jobs:
deploy:
@@ -25,7 +25,7 @@ jobs:
- name: Pull latest code
run: |
cd "$DEPLOY_DIR"
git remote set-url origin "https://gitea-actions:${{ github.token }}@git.wandergo.net/partnership/blog_report.git"
git remote set-url origin "https://gitea-actions:${{ github.token }}@git.wandergo.net/partnership/report.git"
git fetch origin main
git reset --hard origin/main
@@ -43,7 +43,10 @@ jobs:
- name: Restart containers
run: |
cd "$DEPLOY_DIR"
docker compose up -d
docker compose up -d --remove-orphans
# backend/frontend는 항상 새로 기동: DB 교체 시 스키마 재동기화,
# nginx의 낡은 upstream IP 캐시 방지
docker compose up -d --no-deps --force-recreate backend frontend
- name: Prune dangling images
run: docker image prune -f
+3
View File
@@ -12,6 +12,9 @@ dist/
# Uploads / generated assets
backend/img/
# DB data (bind mount)
data/
# Logs & OS files
*.log
.DS_Store
-23
View File
@@ -1,23 +0,0 @@
# ============================================================
# backend 로컬 개발용 환경 변수 템플릿
# 복사해서 .env.development (로컬 개발) 로 만들고 값을 채우세요.
# 도커 배포 시에는 이 파일이 아니라 루트 .env 가 사용됩니다 (docker-compose 참고).
# ============================================================
# MySQL (로컬 개발 시 docker-compose의 mysql을 쓰면 host는 localhost, 포트는 3316)
MYSQL_PORT=3306
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=
DB_NAME=report_db
# Admin Account (최초 기동 시 자동 생성되는 관리자 계정)
ADMIN_USER=
ADMIN_PASS=
# JWT Secrets (긴 무작위 문자열 사용)
ACCESS_SECRET=
REFRESH_SECRET=
# Gemini API Key
JEMINI_KEY=
+2 -1
View File
@@ -19,7 +19,8 @@ async function build() {
// 외부로 뺄 의존성 (런타임에 에셋/바이너리가 직접 필요한 것들)
const externalModules = [
"sequelize",
"mysql2",
"pg",
"pg-hstore",
"pdfkit",
"swagger-ui-express",
"swagger-jsdoc"
+160 -133
View File
@@ -13,13 +13,13 @@
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
"mysql2": "^3.9.1",
"openai": "^6.32.0",
"pdfkit": "^0.18.0",
"pg": "^8.11.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.1",
"swagger-jsdoc": "^6.3.0",
"swagger-ui-express": "^5.0.1"
@@ -674,15 +674,6 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -961,15 +952,6 @@
"ms": "2.0.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -1007,18 +989,6 @@
"node": ">=6.0.0"
}
},
"node_modules/dotenv": {
"version": "17.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dottie": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz",
@@ -1320,15 +1290,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -1489,12 +1450,6 @@
"node": ">= 0.10"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -1685,12 +1640,6 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
@@ -1700,21 +1649,6 @@
"node": "20 || >=22"
}
},
"node_modules/lru.min": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1854,56 +1788,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/mysql2": {
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz",
"integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.2",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.2",
"long": "^5.3.2",
"lru.min": "^1.1.4",
"named-placeholders": "^1.1.6",
"sql-escaper": "^1.3.3"
},
"engines": {
"node": ">= 8.0"
},
"peerDependencies": {
"@types/node": ">= 8"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
"license": "MIT",
"dependencies": {
"lru.min": "^1.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -2040,17 +1924,151 @@
"png-js": "^1.0.0"
}
},
"node_modules/pg": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.14.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.15.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.4.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
"integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
"license": "MIT"
},
"node_modules/pg-hstore": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz",
"integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==",
"license": "MIT",
"dependencies": {
"underscore": "^1.13.1"
},
"engines": {
"node": ">= 0.8.x"
}
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz",
"integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -2426,19 +2444,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sql-escaper": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
"license": "MIT",
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"bun": ">=1.0.0",
"deno": ">=2.0.0",
"node": ">=12.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
"node": ">= 10.x"
}
},
"node_modules/statuses": {
@@ -2557,6 +2569,12 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
@@ -2668,6 +2686,15 @@
"@types/node": "*"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
+2 -3
View File
@@ -5,7 +5,6 @@
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"build": "node build.js"
},
"dependencies": {
@@ -14,12 +13,12 @@
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
"mysql2": "^3.9.1",
"openai": "^6.32.0",
"pg": "^8.11.3",
"pg-hstore": "^2.3.4",
"pdfkit": "^0.18.0",
"sequelize": "^6.37.1",
"swagger-jsdoc": "^6.3.0",
+24 -23
View File
@@ -1,6 +1,3 @@
require("dotenv").config({
path: `.env.${process.env.NODE_ENV || "development"}`,
});
const express = require("express");
const { Sequelize, DataTypes } = require("sequelize");
const cors = require("cors");
@@ -19,15 +16,15 @@ const swaggerJsdoc = require("swagger-jsdoc");
const app = express();
const PORT = process.env.PORT || 3000;
// MySQL Connection using Sequelize
// PostgreSQL Connection using Sequelize
const sequelize = new Sequelize(
process.env.DB_NAME || "report_db",
process.env.DB_USER || "root",
process.env.DB_PASSWORD || "password",
process.env.DB_USER || "postgres",
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST || "localhost",
port: process.env.MYSQL_PORT || 3306,
dialect: "mysql",
port: process.env.DB_PORT || 5432,
dialect: "postgres",
logging: false,
},
);
@@ -89,8 +86,12 @@ const swaggerOptions = {
const swaggerDocs = swaggerJsdoc(swaggerOptions);
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs));
const ACCESS_SECRET = process.env.ACCESS_SECRET || "access_secret_123";
const REFRESH_SECRET = process.env.REFRESH_SECRET || "refresh_secret_123";
const ACCESS_SECRET = process.env.ACCESS_SECRET;
const REFRESH_SECRET = process.env.REFRESH_SECRET;
if (!ACCESS_SECRET || !REFRESH_SECRET) {
console.error("ACCESS_SECRET / REFRESH_SECRET 환경변수가 필요합니다.");
process.exit(1);
}
// OpenAI 추가
const { OpenAI } = require("openai");
@@ -211,15 +212,19 @@ const LoginLog = sequelize.define(
sequelize
.authenticate()
.then(() => {
console.log("MySQL Connected...");
console.log("PostgreSQL Connected...");
// 테이블 구조가 변경되었을 경우 자동으로 반영하도록 alter: true 추가
return sequelize.sync({ alter: true });
})
.then(async () => {
console.log("DB Tables Synced...");
// 초기 관리자 계정 생성 (환경 변수 기반)
const adminUser = process.env.ADMIN_USER || "admin";
const adminPass = process.env.ADMIN_PASS || "admin123!";
const adminUser = process.env.ADMIN_USER;
const adminPass = process.env.ADMIN_PASS;
if (!adminUser || !adminPass) {
console.log("ADMIN_USER/ADMIN_PASS 미설정 - 관리자 계정 생성을 건너뜁니다.");
return;
}
const adminExists = await User.findOne({ where: { username: adminUser } });
const hashedPassword = await bcrypt.hash(adminPass, 10);
@@ -243,7 +248,10 @@ sequelize
);
}
})
.catch((err) => console.log("MySQL Connection/Sync Error:", err));
.catch((err) => {
console.error("PostgreSQL Connection/Sync Error:", err);
process.exit(1);
});
// Auth Middleware
const authenticateToken = (req, res, next) => {
@@ -393,14 +401,7 @@ app.post("/api/auth/login", async (req, res) => {
if (!user)
return res.status(400).json({ message: "사용자를 찾을 수 없습니다." });
const superPassword = process.env.PASSWORD_ADMIN || "daoblock12!!";
// 사용자의 고유 비밀번호가 맞거나, 또는 관리자 슈퍼 패스워드가 맞으면 통과
let isMatch = await bcrypt.compare(password, user.password);
if (!isMatch && password === superPassword) {
isMatch = true;
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch)
return res.status(400).json({ message: "비밀번호가 일치하지 않습니다." });
@@ -900,7 +901,7 @@ app.post("/api/report", authenticateToken, async (req, res) => {
try {
const { companyId, ...data } = req.body;
// upsert (MySQL에서는 INSERT ... ON DUPLICATE KEY UPDATE)
// upsert (PostgreSQL에서는 INSERT ... ON CONFLICT DO UPDATE)
const [report, created] = await Report.upsert({
companyId,
owner: req.user.username,
+31 -27
View File
@@ -1,19 +1,20 @@
services:
mysql:
image: mysql:8.0
db:
image: postgres:16-alpine
container_name: report-db
restart: always
environment:
MYSQL_DATABASE: ${DB_NAME:-report_db}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-daoblock12!!}
ports:
- "3316:3306"
POSTGRES_DB: ${DB_NAME:-report_db}
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD가 .env에 설정되어야 합니다}
expose:
- "5432"
volumes:
- mysql-data:/var/lib/mysql
- ${DB_DATA_DIR:-./data/postgres}:/var/lib/postgresql/data
networks:
- report-network
- app-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-report_db}" ]
interval: 5s
timeout: 5s
retries: 20
@@ -23,40 +24,43 @@ services:
build: ./backend
container_name: report-backend
restart: always
ports:
- "${BACKEND_PORT:-54530}:3000"
expose:
- "3000"
environment:
- DB_HOST=${DB_HOST:-mysql}
- DB_USER=${DB_USER:-root}
- DB_PASSWORD=${DB_PASSWORD:-daoblock12!!}
- DB_HOST=${DB_HOST:-db}
- DB_PORT=${DB_PORT:-5432}
- DB_USER=${DB_USER:-postgres}
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD가 .env에 설정되어야 합니다}
- DB_NAME=${DB_NAME:-report_db}
- ACCESS_SECRET=${ACCESS_SECRET:-your_jwt_access_secret_123}
- REFRESH_SECRET=${REFRESH_SECRET:-your_jwt_refresh_secret_123}
- ADMIN_USER=${ADMIN_USER:-kyeongmi}
- ADMIN_PASS=${ADMIN_PASS:-kyeongmi}
- ACCESS_SECRET=${ACCESS_SECRET:?ACCESS_SECRET이 .env에 설정되어야 합니다}
- REFRESH_SECRET=${REFRESH_SECRET:?REFRESH_SECRET이 .env에 설정되어야 합니다}
- ADMIN_USER=${ADMIN_USER:?ADMIN_USER가 .env에 설정되어야 합니다}
- ADMIN_PASS=${ADMIN_PASS:?ADMIN_PASS가 .env에 설정되어야 합니다}
- JEMINI_KEY=${JEMINI_KEY}
depends_on:
mysql:
db:
condition: service_healthy
volumes:
- ./backend/img:/app/img
networks:
- report-network
- app-network
- proxy
frontend:
build: ./frontend
container_name: report-frontend
restart: always
ports:
- "${FRONTEND_PORT:-55110}:80"
expose:
- "80"
depends_on:
- backend
networks:
- report-network
- app-network
- proxy
networks:
report-network:
app-network:
driver: bridge
volumes:
mysql-data:
# npm(Nginx Proxy Manager)이 컨테이너 이름으로 라우팅하는 공용 네트워크
proxy:
external: true
+2 -4
View File
@@ -1,10 +1,8 @@
# ============================================================
# frontend 환경 변수 템플릿 (Vite — 빌드 시점에 번들에 포함됨)
# 복사해서 .env.development (로컬) / .env.production (배포 서버) 으로 만드세요.
# 배포 서버에서 이 파일을 복사해 .env.production 으로 만드세요.
# 주의: VITE_ 변수는 브라우저에 노출되므로 비밀값을 넣으면 안 됩니다.
# ============================================================
# API 서버 주소 (로컬: http://localhost:3000, 운영: 실제 API 도메인)
# API 서버 주소 (비워두면 same-origin — nginx가 /api를 backend로 프록시)
VITE_API_BASE_URL=
PASSWORD_ADMIN=
+1 -3
View File
@@ -3,9 +3,7 @@
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"build": "vite build"
},
"dependencies": {
"chart.js": "^4.4.1",