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({ ))}
- +