From ee21fe91950c2368bc78debfd11cd6123b12bf2b Mon Sep 17 00:00:00 2001 From: DEV-SERVER Date: Tue, 24 Feb 2026 01:58:32 +0900 Subject: [PATCH 01/10] ci: add Jenkinsfile for CI/CD pipeline (develop/stage/main) --- Jenkinsfile | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..456ee964 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,106 @@ +pipeline { + agent any + + environment { + DEPLOY_USER = 'hskwon' + RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') + } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + stage('Prepare Env') { + steps { + script { + def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" + sh "cp ${envFile} .env.local" + } + } + } + + stage('Install') { + steps { + sh 'npm ci' + } + } + + stage('Build') { + steps { + sh 'npm run build' + } + } + + // ── develop → 개발서버 배포 ── + stage('Deploy Development') { + when { branch 'develop' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + rsync -az --delete \ + .next/ package.json package-lock.json next.config.ts public/ node_modules/ \ + ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ + + scp .env.local ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.local + + ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-front' + """ + } + } + } + + // ── stage → 운영서버 Stage 배포 ── + stage('Deploy Stage') { + when { branch 'stage' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}' + + rsync -az --delete \ + .next/ package.json package-lock.json next.config.ts public/ node_modules/ \ + ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ + + scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.local + + ssh ${DEPLOY_USER}@211.117.60.189 ' + ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && + cd /home/webservice && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 && + cd /home/webservice/react-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + + // ── main → 운영서버 Production 배포 ── + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}' + + rsync -az --delete \ + .next/ package.json package-lock.json next.config.ts public/ node_modules/ \ + ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ + + scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local + + ssh ${DEPLOY_USER}@211.117.60.189 ' + ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && + cd /home/webservice && pm2 reload sam-front && + cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + } + + post { + success { echo "✅ react 배포 완료 (${env.BRANCH_NAME})" } + failure { echo "❌ react 배포 실패 (${env.BRANCH_NAME})" } + } +} From 7c588ee58c472d3eaaba6a59fcbed83ca423aff4 Mon Sep 17 00:00:00 2001 From: DEV-SERVER Date: Tue, 24 Feb 2026 02:05:27 +0900 Subject: [PATCH 02/10] =?UTF-8?q?ci:=20fix=20npm=20ci=20=E2=86=92=20npm=20?= =?UTF-8?q?install=20(package-lock.json=20not=20tracked)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 456ee964..ead090bf 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -22,7 +22,7 @@ pipeline { stage('Install') { steps { - sh 'npm ci' + sh 'npm install --prefer-offline' } } From e2988e91a1a3129254703d03d5ae5dcdcb0a729d Mon Sep 17 00:00:00 2001 From: DEV-SERVER Date: Tue, 24 Feb 2026 02:22:36 +0900 Subject: [PATCH 03/10] =?UTF-8?q?fix(ci):=20rsync=20trailing=20slash=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20PM2=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=84=B8=EC=8A=A4=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rsync source 경로 trailing slash 제거 (.next/, node_modules/, public/ → .next, node_modules, public) - trailing slash로 디렉토리 내용이 root에 풀리는 문제 해결 - dev deploy에 --exclude .git, .env*, ecosystem.config.* 추가 - PM2 프로세스명: sam-front → sam-react (개발서버) --- Jenkinsfile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index ead090bf..a0789999 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -39,12 +39,15 @@ pipeline { sshagent(credentials: ['deploy-ssh-key']) { sh """ rsync -az --delete \ - .next/ package.json package-lock.json next.config.ts public/ node_modules/ \ + --exclude='.git' \ + --exclude='.env*' \ + --exclude='ecosystem.config.*' \ + .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ scp .env.local ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.local - ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-front' + ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react' """ } } @@ -59,7 +62,7 @@ pipeline { ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}' rsync -az --delete \ - .next/ package.json package-lock.json next.config.ts public/ node_modules/ \ + .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.local @@ -83,7 +86,7 @@ pipeline { ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}' rsync -az --delete \ - .next/ package.json package-lock.json next.config.ts public/ node_modules/ \ + .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local @@ -100,7 +103,7 @@ pipeline { } post { - success { echo "✅ react 배포 완료 (${env.BRANCH_NAME})" } - failure { echo "❌ react 배포 실패 (${env.BRANCH_NAME})" } + success { echo '✅ react 배포 완료 (' + env.BRANCH_NAME + ')' } + failure { echo '❌ react 배포 실패 (' + env.BRANCH_NAME + ')' } } } From bd7cb4c3011ebeed9c27cb1eafaa23c5120b6fa6 Mon Sep 17 00:00:00 2001 From: DEV-SERVER Date: Tue, 24 Feb 2026 13:20:48 +0900 Subject: [PATCH 04/10] =?UTF-8?q?refactor:=20stage=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EC=A0=9C=EA=B1=B0,=20main=EC=97=90=EC=84=9C=20Stag?= =?UTF-8?q?e=E2=86=92=EC=8A=B9=EC=9D=B8=E2=86=92Production=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=ED=9D=90=EB=A6=84=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - develop: 개발서버 자동 배포 (변경 없음) - main: Stage 자동 배포 → Jenkins 승인 → Production 재빌드+배포 - stage 브랜치 더 이상 사용 안함 - Next.js는 빌드 시 env 바인딩되므로 Stage/Production 별도 빌드 --- Jenkinsfile | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a0789999..ef4607be 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,22 +14,22 @@ pipeline { stage('Prepare Env') { steps { script { - def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" - sh "cp ${envFile} .env.local" + if (env.BRANCH_NAME == 'main') { + sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.local" + } else { + def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" + sh "cp ${envFile} .env.local" + } } } } stage('Install') { - steps { - sh 'npm install --prefer-offline' - } + steps { sh 'npm install --prefer-offline' } } stage('Build') { - steps { - sh 'npm run build' - } + steps { sh 'npm run build' } } // ── develop → 개발서버 배포 ── @@ -53,9 +53,9 @@ pipeline { } } - // ── stage → 운영서버 Stage 배포 ── + // ── main → 운영서버 Stage 배포 ── stage('Deploy Stage') { - when { branch 'stage' } + when { branch 'main' } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ @@ -77,6 +77,26 @@ pipeline { } } + // ── 운영 배포 승인 ── + stage('Production Approval') { + when { branch 'main' } + steps { + timeout(time: 24, unit: 'HOURS') { + input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr', + ok: '운영 배포 진행' + } + } + } + + // ── main → Production 재빌드 (운영 환경변수) ── + stage('Rebuild for Production') { + when { branch 'main' } + steps { + sh "cp /var/lib/jenkins/env-files/react/.env.main .env.local" + sh 'npm run build' + } + } + // ── main → 운영서버 Production 배포 ── stage('Deploy Production') { when { branch 'main' } From 49d07914fd628c22b1f735b5178a787e9c87ac57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Tue, 24 Feb 2026 21:55:15 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat(WEB):=20CEO=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81,=20?= =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EA=B0=95=ED=99=94,=20validation?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EB=B6=84=EB=A6=AC,=20Git=20Workflow=20?= =?UTF-8?q?=EC=A0=95=EB=A6=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CEO 대시보드 전 섹션 공통 컴포넌트 기반 리팩토링 (SectionCard, StatItem 등) - CalendarSection 일정 CRUD 기능 확장 - validation.ts → validation/ 모듈 분리 (item-schemas, form-schemas, common, utils) - CLAUDE.md Git Workflow 섹션 추가 (develop/main 플로우 정의) - Jenkinsfile CI/CD 파이프라인 정비 (Slack 알림 추가) Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 62 ++++ Jenkinsfile | 31 +- src/components/auth/SignupPage.tsx | 5 +- .../business/CEODashboard/components.tsx | 135 +++++-- .../dialogs/DashboardSettingsDialog.tsx | 2 +- .../modals/ScheduleDetailModal.tsx | 77 ++-- .../CEODashboard/sections/CalendarSection.tsx | 279 +++++++++++--- .../sections/CardManagementSection.tsx | 72 ++-- .../sections/ConstructionSection.tsx | 69 ++-- .../sections/DailyAttendanceSection.tsx | 112 +++--- .../sections/DailyProductionSection.tsx | 229 +++++------- .../sections/DailyReportSection.tsx | 41 +-- .../sections/DebtCollectionSection.tsx | 62 ++-- .../sections/EnhancedSections.tsx | 273 ++++++-------- .../sections/EntertainmentSection.tsx | 60 ++- .../sections/MonthlyExpenseSection.tsx | 46 +-- .../sections/PurchaseStatusSection.tsx | 197 +++++----- .../sections/ReceivableSection.tsx | 84 ++--- .../sections/SalesStatusSection.tsx | 190 ++++------ .../sections/StatusBoardSection.tsx | 30 +- .../sections/TodayIssueSection.tsx | 63 ++-- .../sections/UnshippedSection.tsx | 113 +++--- .../CEODashboard/sections/VatSection.tsx | 46 +-- .../CEODashboard/sections/WelfareSection.tsx | 60 ++- .../hr/EmployeeManagement/EmployeeForm.tsx | 5 +- .../AddCompanyDialog.tsx | 3 +- .../PaymentHistoryManagement/utils.ts | 8 +- .../settings/PopupManagement/utils.ts | 7 +- src/lib/formatters.ts | 39 +- src/lib/utils/date.ts | 11 + src/lib/utils/validation/common.ts | 110 ++++++ src/lib/utils/validation/form-schemas.ts | 191 ++++++++++ src/lib/utils/validation/index.ts | 31 ++ .../item-schemas.ts} | 342 +----------------- src/lib/utils/validation/utils.ts | 64 ++++ 35 files changed, 1648 insertions(+), 1501 deletions(-) create mode 100644 src/lib/utils/validation/common.ts create mode 100644 src/lib/utils/validation/form-schemas.ts create mode 100644 src/lib/utils/validation/index.ts rename src/lib/utils/{validation.ts => validation/item-schemas.ts} (52%) create mode 100644 src/lib/utils/validation/utils.ts diff --git a/CLAUDE.md b/CLAUDE.md index ca070ed4..e7fed7c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,68 @@ sam_project: --- +## Git Workflow +**Priority**: 🔴 + +### 브랜치 구조 +| 브랜치 | 역할 | 커밋 상태 | +|--------|------|-----------| +| `develop` | 평소 작업 브랜치 (자유롭게) | 지저분해도 OK | +| `stage` | QA/테스트 환경 | 기능별 squash 정리 | +| `main` | 배포용 (기본 브랜치) | 검증된 것만 | +| `feature/*` | 큰 기능/실험적 작업 시 | 선택적 사용 | + +### "git 올려줘" 단축 명령어 +`git 올려줘` 입력 시 **develop에 push**: +1. `git status` → 2. `git diff --stat` → 3. `git add -A` → 4. `git commit` (자동 메시지) → 5. `git push origin develop` + +- `snapshot.txt`, `.DS_Store` 파일은 항상 제외 +- develop에서 자유롭게 커밋 (커밋 메시지 정리 불필요) + +### main에 올리기 (기능별 squash merge) +사용자가 "main에 올려줘" 또는 특정 기능을 main에 올리라고 지시할 때만 실행. +**절대 자동으로 main에 push하지 않음.** + +```bash +# 기능별로 squash merge +git checkout main +git merge --squash develop # 또는 cherry-pick으로 특정 커밋만 선별 +git commit -m "feat: [기능명]" +git push origin main +git checkout develop +``` + +기능별로 나눠서 올리는 경우: +```bash +# 예: "대시보드랑 거래처 main에 올려줘" +git checkout main +git cherry-pick --no-commit <대시보드커밋1> <대시보드커밋2> +git commit -m "feat: CEO 대시보드 캘린더 기능 구현" + +git cherry-pick --no-commit <거래처커밋1> <거래처커밋2> +git commit -m "feat: 거래처 관리 개선" + +git push origin main +git checkout develop +``` + +**핵심: main에는 기능 단위 커밋만 → 문제 시 `git revert`로 해당 기능만 롤백 가능** + +### feature 브랜치 사용 기준 +| 상황 | 방법 | +|------|------| +| 일반 작업 | develop에서 바로 | +| 1주일+ 걸리는 큰 기능 | feature/* 따서 작업 | +| 실험적 시도 | feature/* 따서 작업 | +| 백엔드와 동시 수정 건 | 각자 feature/* 권장 | + +### 금지 사항 +- ❌ main에 직접 커밋/push +- ❌ `git push --force` (main/develop) +- ❌ 사용자 지시 없이 main에 merge + +--- + ## Client Component 사용 원칙 **Priority**: 🔴 diff --git a/Jenkinsfile b/Jenkinsfile index ef4607be..7780d3f9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,13 +8,18 @@ pipeline { stages { stage('Checkout') { - steps { checkout scm } + steps { + slackSend channel: '#product_infra', color: '#439FE0', + message: "🚀 *react* 빌드 시작 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + checkout scm + } } stage('Prepare Env') { steps { script { if (env.BRANCH_NAME == 'main') { + // main: Stage 빌드 먼저 (승인 후 Production 재빌드) sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.local" } else { def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" @@ -39,14 +44,10 @@ pipeline { sshagent(credentials: ['deploy-ssh-key']) { sh """ rsync -az --delete \ - --exclude='.git' \ - --exclude='.env*' \ - --exclude='ecosystem.config.*' \ + --exclude='.git' --exclude='.env*' --exclude='ecosystem.config.*' \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ - scp .env.local ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.local - ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react' """ } @@ -60,13 +61,10 @@ pipeline { sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}' - rsync -az --delete \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ - scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.local - ssh ${DEPLOY_USER}@211.117.60.189 ' ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && cd /home/webservice && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 && @@ -104,13 +102,10 @@ pipeline { sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}' - rsync -az --delete \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ - scp .env.local ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.local - ssh ${DEPLOY_USER}@211.117.60.189 ' ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && cd /home/webservice && pm2 reload sam-front && @@ -123,7 +118,13 @@ pipeline { } post { - success { echo '✅ react 배포 완료 (' + env.BRANCH_NAME + ')' } - failure { echo '❌ react 배포 실패 (' + env.BRANCH_NAME + ')' } + success { + slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token', + message: "✅ *react* 배포 성공 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + } + failure { + slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token', + message: "❌ *react* 배포 실패 (`${env.BRANCH_NAME}`)\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + } } -} +} \ No newline at end of file diff --git a/src/components/auth/SignupPage.tsx b/src/components/auth/SignupPage.tsx index a7fc3465..17412831 100644 --- a/src/components/auth/SignupPage.tsx +++ b/src/components/auth/SignupPage.tsx @@ -25,6 +25,7 @@ import { } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { extractDigits } from '@/lib/formatters'; export function SignupPage() { const router = useRouter(); @@ -64,7 +65,7 @@ export function SignupPage() { // 사업자등록번호 자동 포맷팅 (000-00-00000) const formatBusinessNumber = (value: string) => { // 숫자만 추출 - const numbers = value.replace(/[^\d]/g, ''); + const numbers = extractDigits(value); // 최대 10자리까지만 const limited = numbers.slice(0, 10); @@ -87,7 +88,7 @@ export function SignupPage() { // 핸드폰 번호 자동 포맷팅 (010-1111-1111 or 010-111-1111) const formatPhoneNumber = (value: string) => { // 숫자만 추출 - const numbers = value.replace(/[^\d]/g, ''); + const numbers = extractDigits(value); // 최대 11자리까지만 const limited = numbers.slice(0, 11); diff --git a/src/components/business/CEODashboard/components.tsx b/src/components/business/CEODashboard/components.tsx index 924e9523..52c4d1c2 100644 --- a/src/components/business/CEODashboard/components.tsx +++ b/src/components/business/CEODashboard/components.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import { Check, AlertTriangle, @@ -7,6 +8,7 @@ import { AlertCircle, TrendingUp, TrendingDown, + ChevronDown, type LucideIcon, } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; @@ -18,18 +20,18 @@ import type { CheckPoint, CheckPointType, AmountCard, HighlightColor } from './t // 섹션별 컬러 테마 타입 export type SectionColorTheme = 'blue' | 'purple' | 'orange' | 'green' | 'red' | 'amber' | 'cyan' | 'pink' | 'emerald' | 'indigo'; -// 컬러 테마별 스타일 -export const SECTION_THEME_STYLES: Record = { - blue: { bg: '#eff6ff', border: '#bfdbfe', iconBg: '#3b82f6', labelColor: '#1d4ed8', accentColor: '#3b82f6' }, - purple: { bg: '#faf5ff', border: '#e9d5ff', iconBg: '#a855f7', labelColor: '#7c3aed', accentColor: '#a855f7' }, - orange: { bg: '#fff7ed', border: '#fed7aa', iconBg: '#f97316', labelColor: '#ea580c', accentColor: '#f97316' }, - green: { bg: '#f0fdf4', border: '#bbf7d0', iconBg: '#22c55e', labelColor: '#16a34a', accentColor: '#22c55e' }, - red: { bg: '#fef2f2', border: '#fecaca', iconBg: '#ef4444', labelColor: '#dc2626', accentColor: '#ef4444' }, - amber: { bg: '#fffbeb', border: '#fde68a', iconBg: '#f59e0b', labelColor: '#d97706', accentColor: '#f59e0b' }, - cyan: { bg: '#ecfeff', border: '#a5f3fc', iconBg: '#06b6d4', labelColor: '#0891b2', accentColor: '#06b6d4' }, - pink: { bg: '#fdf2f8', border: '#fbcfe8', iconBg: '#ec4899', labelColor: '#db2777', accentColor: '#ec4899' }, - emerald: { bg: '#ecfdf5', border: '#a7f3d0', iconBg: '#10b981', labelColor: '#059669', accentColor: '#10b981' }, - indigo: { bg: '#eef2ff', border: '#c7d2fe', iconBg: '#6366f1', labelColor: '#4f46e5', accentColor: '#6366f1' }, +// 컬러 테마별 스타일 (다크모드 지원 Tailwind 클래스) +export const SECTION_THEME_STYLES: Record = { + blue: { bgClass: 'bg-blue-50 dark:bg-blue-900/30', borderClass: 'border-blue-200 dark:border-blue-800', iconBg: '#3b82f6', labelClass: 'text-blue-700 dark:text-blue-300', accentColor: '#3b82f6' }, + purple: { bgClass: 'bg-purple-50 dark:bg-purple-900/30', borderClass: 'border-purple-200 dark:border-purple-800', iconBg: '#a855f7', labelClass: 'text-purple-700 dark:text-purple-300', accentColor: '#a855f7' }, + orange: { bgClass: 'bg-orange-50 dark:bg-orange-900/30', borderClass: 'border-orange-200 dark:border-orange-800', iconBg: '#f97316', labelClass: 'text-orange-700 dark:text-orange-300', accentColor: '#f97316' }, + green: { bgClass: 'bg-green-50 dark:bg-green-900/30', borderClass: 'border-green-200 dark:border-green-800', iconBg: '#22c55e', labelClass: 'text-green-700 dark:text-green-300', accentColor: '#22c55e' }, + red: { bgClass: 'bg-red-50 dark:bg-red-900/30', borderClass: 'border-red-200 dark:border-red-800', iconBg: '#ef4444', labelClass: 'text-red-700 dark:text-red-300', accentColor: '#ef4444' }, + amber: { bgClass: 'bg-amber-50 dark:bg-amber-900/30', borderClass: 'border-amber-200 dark:border-amber-800', iconBg: '#f59e0b', labelClass: 'text-amber-700 dark:text-amber-300', accentColor: '#f59e0b' }, + cyan: { bgClass: 'bg-cyan-50 dark:bg-cyan-900/30', borderClass: 'border-cyan-200 dark:border-cyan-800', iconBg: '#06b6d4', labelClass: 'text-cyan-700 dark:text-cyan-300', accentColor: '#06b6d4' }, + pink: { bgClass: 'bg-pink-50 dark:bg-pink-900/30', borderClass: 'border-pink-200 dark:border-pink-800', iconBg: '#ec4899', labelClass: 'text-pink-700 dark:text-pink-300', accentColor: '#ec4899' }, + emerald: { bgClass: 'bg-emerald-50 dark:bg-emerald-900/30', borderClass: 'border-emerald-200 dark:border-emerald-800', iconBg: '#10b981', labelClass: 'text-emerald-700 dark:text-emerald-300', accentColor: '#10b981' }, + indigo: { bgClass: 'bg-indigo-50 dark:bg-indigo-900/30', borderClass: 'border-indigo-200 dark:border-indigo-800', iconBg: '#6366f1', labelClass: 'text-indigo-700 dark:text-indigo-300', accentColor: '#6366f1' }, }; /** @@ -249,31 +251,21 @@ export const AmountCardItem = ({ return formatKoreanAmount(amount); }; - // 테마 적용 시 스타일 - const cardStyle = themeStyle && !card.isHighlighted ? { - backgroundColor: themeStyle.bg, - borderColor: themeStyle.border, - } : undefined; - return ( {/* 건수 뱃지 (오른쪽 상단) */} {showCountBadge && card.subLabel && (
{card.subLabel}
@@ -299,9 +291,8 @@ export const AmountCardItem = ({

{card.label}

@@ -309,7 +300,7 @@ export const AmountCardItem = ({ {/* 금액 */}

{formatCardAmount(card.amount)} @@ -318,11 +309,12 @@ export const AmountCardItem = ({ {/* 트렌드 표시 (pill 형태, 금액 아래에 배치) */} {showTrend && trendValue && (

{trendDirection === 'up' ? ( @@ -360,10 +352,12 @@ export const AmountCardItem = ({ {card.subLabel && card.subAmount === undefined && !card.previousLabel && ( subLabelAsBadge && themeStyle ? ( @@ -431,4 +425,69 @@ export const IssueCardItem = ({ ); -}; \ No newline at end of file +}; + +/** + * 접기/펼치기 가능한 대시보드 카드 + * - 다크 헤더 + 흰색 바디 패턴의 공통 컴포넌트 + * - 헤더 클릭 시 바디 토글 + */ +interface CollapsibleDashboardCardProps { + icon: React.ReactNode; + title: string; + subtitle?: string; + rightElement?: React.ReactNode; + children: React.ReactNode; + defaultOpen?: boolean; + bodyClassName?: string; +} + +export function CollapsibleDashboardCard({ + icon, + title, + subtitle, + rightElement, + children, + defaultOpen = true, + bodyClassName, +}: CollapsibleDashboardCardProps) { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( +
+
setIsOpen(!isOpen)} + > +
+
+
+ {icon} +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+
+ {rightElement} + +
+
+
+ {isOpen && ( +
+ {children} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx b/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx index 85ddff71..96d0b139 100644 --- a/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx +++ b/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx @@ -889,7 +889,7 @@ export function DashboardSettingsDialog({ ))}
- +