From 8f6baeb5f92c8c700f184b8c31966c59b0bbeb80 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Sun, 22 Jun 2025 19:47:37 +0900 Subject: [PATCH 01/19] =?UTF-8?q?=E2=9C=A8=20feat:=20ci=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 76 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..370f676 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: CI +on: + push: + branches: + - feat/ci-cd # TODO: main으로 변경 필요 + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + # 4. Next.js 빌드 + - name: Build Next.js application + run: CI=false npm run build + + # 5. Docker Buildx 설정 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # 6. Docker Hub 로그인 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # 7. Docker 이미지 빌드 및 푸시 + - name: Build and push Docker image + id: docker_build + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/leadme5-front:latest + + # 8. Docker Digest 값 추출 (배포 설정에 사용할 이미지 고유 식별자) + - name: Get image digest + id: get_digest + run: echo "IMAGE_DIGEST=${{ steps.docker_build.outputs.digest }}" >> $GITHUB_ENV + + # 9. 인프라 레포지토리 클론 + - name: Checkout infra-k8s repository + uses: actions/checkout@v4 + with: + repository: LEADME-skala5/infra-k8s + path: infra-k8s + token: ${{ secrets.GIT_PAT }} + + # 10. deployment.yaml 파일 내 이미지 경로를 새 Digest로 치환 + - name: Update infra-k8s deployment.yaml with new image digest + run: | + echo "Updating deployment.yaml with new image digest: ${{ env.IMAGE_DIGEST }}" + sed -i 's|image: .*/leadme5-front.*|image: ${{ secrets.DOCKERHUB_USERNAME }}/leadme5-front@${{ env.IMAGE_DIGEST }}|g' infra-k8s/front/deployment.yaml + + # 11. 변경사항 커밋 및 푸시 + - name: Commit and push updated deployment.yaml + run: | + cd infra-k8s + git config user.name "SanghyunLee" + git config user.email "lgw9736@naver.com" + git add front/deployment.yaml + git commit -m "Update front deployment image to ${{ env.IMAGE_DIGEST }}" + git push origin main From dc441d3e72f5bda70dcd5f08765df0d2c09fcb19 Mon Sep 17 00:00:00 2001 From: soohee Jang Date: Wed, 25 Jun 2025 16:02:14 +0900 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/actions.ts | 30 ++++++++++ app/team/overview/page.tsx | 83 ++++++++++++++++++++++---- components/auth/login-form.tsx | 35 +++-------- components/team/team-evaluation.tsx | 81 +++++++------------------ components/team/team-report.tsx | 91 ++++++++--------------------- 5 files changed, 153 insertions(+), 167 deletions(-) create mode 100644 app/login/actions.ts diff --git a/app/login/actions.ts b/app/login/actions.ts new file mode 100644 index 0000000..5e401c7 --- /dev/null +++ b/app/login/actions.ts @@ -0,0 +1,30 @@ +// loginAction.ts 수정 +'use server'; + +import { redirect } from 'next/navigation'; +import { cookies } from 'next/headers'; + +export async function loginAction({ userId, password }: { userId: string; password: string }) { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ employeeNumber: userId, password }), + credentials: 'include', + }); + + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.message || '로그인 실패'); + } + + (await cookies()).set('accessToken', data.accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60, + sameSite: 'strict', + path: '/', + }); + + redirect('/dashboard'); +} diff --git a/app/team/overview/page.tsx b/app/team/overview/page.tsx index 0e02693..0a384a5 100644 --- a/app/team/overview/page.tsx +++ b/app/team/overview/page.tsx @@ -1,19 +1,80 @@ +// 수정된 page.tsx import { TeamEvaluation } from '@/components/team/team-evaluation'; import { TeamReport } from '@/components/team/team-report'; +import { cookies } from 'next/headers'; -// Mock API call - replace with your actual API -async function getTeamEvaluationStatus() { - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 100)); +interface Task { + taskId: number; + name: string; + isEvaluated: boolean; + grade: number | null; +} + +interface User { + userId: number; + name: string; + position: string; + email: string; + tasks: Task[]; + quarterScore: number | null; + lastUpdated: string | null; +} + +interface ApiResponse { + evaluated: boolean; + users: User[]; +} - // Mock data - replace with actual API response - // Return true if team has been evaluated, false if not - return Math.random() > 0.5; // Random for demonstration +interface PageProps { + params: { organizationId: string }; } -export default async function TeamOverviewPage() { - // const hasBeenEvaluated = await getTeamEvaluationStatus(); - const hasBeenEvaluated = true; +async function getEvaluationData(organizationId: string): Promise { + try { + const cookieStore = await cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/quantitative-evaluation/${organizationId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + cache: 'no-store', + } + ); + console.log('accessToken', accessToken); + + if (!res.ok) throw new Error('API 요청 실패'); + return await res.json(); + } catch (error) { + console.error(error); + return { evaluated: false, users: [] }; + } +} + +export default async function Page({ params }: PageProps) { + const { organizationId } = params; + const { evaluated, users } = await getEvaluationData(organizationId); + + // API 응답 → 컴포넌트 데이터 형식 변환 + const teamMembers = users.map((user) => ({ + id: user.userId.toString(), + name: user.name, + role: user.position, + email: user.email, + projects: user.tasks.map((task) => task.name), + performanceScore: user.quarterScore || 0, // null 대체값 + lastEvaluationDate: user.lastUpdated || '', // null 대체값 + })); - return
{hasBeenEvaluated ? : }
; + return ( +
+ {evaluated ? ( + + ) : ( + + )} +
+ ); } diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index bc89853..25f1934 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -12,10 +12,10 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { Eye, EyeOff, LogIn } from 'lucide-react'; import Link from 'next/link'; import { useUserStore } from '@/store/useUserStore'; +import { loginAction } from '@/app/login/actions'; export function LoginForm() { const setUser = useUserStore((state) => state.setUser); - const router = useRouter(); const [formData, setFormData] = useState({ userId: '', password: '', @@ -50,33 +50,14 @@ export function LoginForm() { setErrors({}); try { - const res = await fetch('/api/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userId: formData.userId, - password: formData.password, - }), - credentials: 'include', + //loginAction 호출로 변경 + await loginAction({ + userId: formData.userId, + password: formData.password, }); - - const data = await res.json(); - - if (!res.ok) { - setErrors({ general: data.error || '로그인 실패' }); - return; - } - - const user = data.user; - const accessToken = data.accessToken; - - setUser(user, accessToken); - localStorage.setItem('user', JSON.stringify(user)); - router.push('/dashboard'); - } catch (error) { - setErrors({ general: '서버 연결 오류' }); + // redirect는 loginAction 내부에서 처리됨 + } catch (error: any) { + setErrors({ general: error.message || '로그인 실패' }); } finally { setIsLoading(false); } diff --git a/components/team/team-evaluation.tsx b/components/team/team-evaluation.tsx index beb076e..6589191 100644 --- a/components/team/team-evaluation.tsx +++ b/components/team/team-evaluation.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -21,72 +21,29 @@ interface TeamMember { role: string; email: string; projects: string[]; - avatar?: string; } -const mockTeamMembers: TeamMember[] = [ - { - id: '1', - name: '김민수', - role: 'Senior Developer', - email: 'kim.minsu@company.com', - projects: ['Project Alpha', 'API Integration'], - }, - { - id: '2', - name: '이지영', - role: 'UI/UX Designer', - email: 'lee.jiyoung@company.com', - projects: ['Project Alpha', 'Dashboard UI'], - }, - { - id: '3', - name: '박준호', - role: 'Backend Developer', - email: 'park.junho@company.com', - projects: ['Project Beta', 'API Integration'], - }, - { - id: '4', - name: '최수진', - role: 'Product Manager', - email: 'choi.sujin@company.com', - projects: ['Project Alpha', 'Dashboard UI'], - }, - { - id: '5', - name: '정현우', - role: 'Frontend Developer', - email: 'jung.hyunwoo@company.com', - projects: ['Dashboard UI'], - }, - { - id: '6', - name: '한소영', - role: 'QA Engineer', - email: 'han.soyoung@company.com', - projects: ['Project Alpha', 'Project Beta'], - }, -]; +interface TeamEvaluationProps { + teamMembers: TeamMember[]; // props로 팀 멤버 데이터 받기 +} type SortMode = 'alphabetical' | 'role'; -export function TeamEvaluation() { +export function TeamEvaluation({ teamMembers: initialTeamMembers }: TeamEvaluationProps) { const router = useRouter(); const [sortMode, setSortMode] = useState('alphabetical'); - const [teamMembers, setTeamMembers] = useState(mockTeamMembers); - const sortTeamMembers = (mode: SortMode) => { - const sorted = [...mockTeamMembers].sort((a, b) => { - if (mode === 'alphabetical') { + // 1. props로 전달된 initialTeamMembers 사용 + // 2. useMemo로 정렬된 팀원 목록 계산 + const sortedTeamMembers = useMemo(() => { + return [...initialTeamMembers].sort((a, b) => { + if (sortMode === 'alphabetical') { return a.name.localeCompare(b.name); } else { return a.role.localeCompare(b.role); } }); - setTeamMembers(sorted); - setSortMode(mode); - }; + }, [initialTeamMembers, sortMode]); const handleQualitativeEvaluation = (memberId: string) => { router.push(`/team/member/${memberId}/evaluate`); @@ -100,6 +57,10 @@ export function TeamEvaluation() { .toUpperCase(); }; + // 3. 통계 데이터 계산 (실제 데이터 기반) + const totalMembers = initialTeamMembers.length; + const uniqueProjects = new Set(initialTeamMembers.flatMap((m) => m.projects)).size; + return (
{/* Header */} @@ -117,7 +78,7 @@ export function TeamEvaluation() { {/* Sort Controls */}
정렬: - setSortMode(value)}> @@ -136,7 +97,7 @@ export function TeamEvaluation() {

평가 대상 팀원

-

{teamMembers.length}

+

{totalMembers}

@@ -160,9 +121,7 @@ export function TeamEvaluation() {

진행 중인 프로젝트

-

- {new Set(teamMembers.flatMap((m) => m.projects)).size} -

+

{uniqueProjects}

@@ -172,7 +131,7 @@ export function TeamEvaluation() { {/* Team Members List */}
- {teamMembers.map((member) => ( + {sortedTeamMembers.map((member) => (
- + {/* */} {getInitials(member.name)} diff --git a/components/team/team-report.tsx b/components/team/team-report.tsx index 10a8585..9736428 100644 --- a/components/team/team-report.tsx +++ b/components/team/team-report.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -23,84 +23,39 @@ interface TeamMember { projects: string[]; performanceScore: number; lastEvaluationDate: string; - avatar?: string; } -const mockTeamMembers: TeamMember[] = [ - { - id: '1', - name: '김민수', - role: 'Senior Developer', - email: 'kim.minsu@company.com', - projects: ['Project Alpha', 'API Integration'], - performanceScore: 4.5, - lastEvaluationDate: '2024-01-15', - }, - { - id: '2', - name: '이지영', - role: 'UI/UX Designer', - email: 'lee.jiyoung@company.com', - projects: ['Project Alpha', 'Dashboard UI'], - performanceScore: 4.8, - lastEvaluationDate: '2024-01-12', - }, - { - id: '3', - name: '박준호', - role: 'Backend Developer', - email: 'park.junho@company.com', - projects: ['Project Beta', 'API Integration'], - performanceScore: 4.2, - lastEvaluationDate: '2024-01-10', - }, - { - id: '4', - name: '최수진', - role: 'Product Manager', - email: 'choi.sujin@company.com', - projects: ['Project Alpha', 'Dashboard UI'], - performanceScore: 4.6, - lastEvaluationDate: '2024-01-08', - }, - { - id: '5', - name: '정현우', - role: 'Frontend Developer', - email: 'jung.hyunwoo@company.com', - projects: ['Dashboard UI'], - performanceScore: 3.9, - lastEvaluationDate: '2024-01-05', - }, - { - id: '6', - name: '한소영', - role: 'QA Engineer', - email: 'han.soyoung@company.com', - projects: ['Project Alpha', 'Project Beta'], - performanceScore: 4.3, - lastEvaluationDate: '2024-01-03', - }, -]; +interface TeamReportProps { + teamMembers: TeamMember[]; +} type SortMode = 'alphabetical' | 'performance'; -export function TeamReport() { +export function TeamReport({ teamMembers }: TeamReportProps) { const router = useRouter(); const [sortMode, setSortMode] = useState('alphabetical'); - const [teamMembers, setTeamMembers] = useState(mockTeamMembers); - const sortTeamMembers = (mode: SortMode) => { - const sorted = [...mockTeamMembers].sort((a, b) => { - if (mode === 'alphabetical') { + // 2. useMemo로 정렬된 팀원 목록 계산 + const sortedTeamMembers = useMemo(() => { + return [...teamMembers].sort((a, b) => { + if (sortMode === 'alphabetical') { return a.name.localeCompare(b.name); } else { return b.performanceScore - a.performanceScore; } }); - setTeamMembers(sorted); - setSortMode(mode); - }; + }, [teamMembers, sortMode]); + + // 3. 통계 데이터 계산 + const totalMembers = teamMembers.length; + const highPerformers = teamMembers.filter((m) => m.performanceScore >= 4.5).length; + const averageScore = + teamMembers.length > 0 + ? (teamMembers.reduce((sum, m) => sum + m.performanceScore, 0) / teamMembers.length).toFixed( + 1 + ) + : '0.0'; + const uniqueProjects = new Set(teamMembers.flatMap((m) => m.projects)).size; const handleViewReport = (memberId: string) => { router.push(`/team/member/${memberId}`); @@ -144,7 +99,7 @@ export function TeamReport() { {/* Sort Controls */}
정렬: - setSortMode(value)}> @@ -226,7 +181,7 @@ export function TeamReport() {
- + {/* */} {getInitials(member.name)} From 629a6a2ca3f162827b62ae72e94a9843c3ac3e34 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 17:37:58 +0900 Subject: [PATCH 03/19] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=EC=8B=9C=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 370f676..d64e1ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: # 4. Next.js 빌드 - name: Build Next.js application run: CI=false npm run build + env: + NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} # 5. Docker Buildx 설정 - name: Set up Docker Buildx From 02070408563d90744fc80a0a63ac8b706369f195 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 17:43:42 +0900 Subject: [PATCH 04/19] =?UTF-8?q?=E2=9C=A8=20feat:=20DockerFile=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7e49f63 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# 1️⃣ Build Stage +FROM node:20-alpine AS builder + +# 작업 디렉토리 +WORKDIR /app + +# 필요한 파일 복사 +COPY package.json pnpm-lock.yaml ./ +RUN npm install -g pnpm && pnpm install + +# 전체 소스 복사 +COPY . . + +# Next.js 빌드 +RUN pnpm run build + +# 2️⃣ Production Stage +FROM node:20-alpine AS runner +WORKDIR /app + +# 필요한 파일만 복사 +COPY --from=builder /app/.next .next +COPY --from=builder /app/public public +COPY --from=builder /app/node_modules node_modules +COPY --from=builder /app/package.json package.json +COPY --from=builder /app/next.config.js next.config.js + +# 포트 설정 +EXPOSE 3000 +ENV NODE_ENV=production + +# 실행 +CMD ["pnpm", "start"] From 9ead5902567cbced25e3a9f81b30696840aad493 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 17:50:28 +0900 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20=EA=B9=83=20=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d64e1ea..763b926 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,9 @@ jobs: context: . push: true tags: ${{ secrets.DOCKERHUB_USERNAME }}/leadme5-front:latest + build-args: | + NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} + JWT_SECRET=${{ secrets.JWT_SECRET }} # 8. Docker Digest 값 추출 (배포 설정에 사용할 이미지 고유 식별자) - name: Get image digest From 0df0f9f34fbfefc1f609125cc8e3edd7f615cc40 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 19:04:01 +0900 Subject: [PATCH 06/19] =?UTF-8?q?fix:=20=EB=8F=84=EC=BB=A4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=82=B4=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Dockerfile b/Dockerfile index 7e49f63..d6a718e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,14 @@ # 1️⃣ Build Stage FROM node:20-alpine AS builder +# Build 시 필요한 변수 정의 +ARG NEXT_PUBLIC_API_URL +ARG JWT_SECRET +# NEXT_PUBLIC_API_URL은 NEXT.js가 정적 페이지 생성 시 사용 +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL +# JWT_SECRET은 서버 로직에서만 사용할 것이지만, 원하면 그대로 ENV로도 지정 가능 +ENV JWT_SECRET=$JWT_SECRET + # 작업 디렉토리 WORKDIR /app @@ -28,6 +36,10 @@ COPY --from=builder /app/next.config.js next.config.js # 포트 설정 EXPOSE 3000 ENV NODE_ENV=production +# NEXT_PUBLIC_API_URL은 그대로 유지 +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL +# JWT_SECRET도 원하면 그대로 ENV로 전달 가능 (서버 사이드 로직에서만 접근 가능!) +ENV JWT_SECRET=$JWT_SECRET # 실행 CMD ["pnpm", "start"] From 3982b55a232921f47acb109b17a7fd2cb779447c Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 19:08:47 +0900 Subject: [PATCH 07/19] =?UTF-8?q?fix:=20=EB=8F=84=EC=BB=A4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 파일 확장자 수정 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d6a718e..14c8a25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ COPY --from=builder /app/.next .next COPY --from=builder /app/public public COPY --from=builder /app/node_modules node_modules COPY --from=builder /app/package.json package.json -COPY --from=builder /app/next.config.js next.config.js +COPY --from=builder /app/next.config.js next.config.mjs # 포트 설정 EXPOSE 3000 From dff1376995cd98ef58407b1dbf7c5eec7e13307c Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 19:12:33 +0900 Subject: [PATCH 08/19] =?UTF-8?q?fix:=20=EB=8F=84=EC=BB=A4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 파일 확장자 수정 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 14c8a25..8cee75f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ COPY --from=builder /app/.next .next COPY --from=builder /app/public public COPY --from=builder /app/node_modules node_modules COPY --from=builder /app/package.json package.json -COPY --from=builder /app/next.config.js next.config.mjs +COPY --from=builder /app/next.config.mjs next.config.mjs # 포트 설정 EXPOSE 3000 From 10960b3eb82737c6a99b9ed23ae750bf17605c21 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 19:19:40 +0900 Subject: [PATCH 09/19] =?UTF-8?q?fix:=20=EB=8F=84=EC=BB=A4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pnpm 설치 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8cee75f..6a1eeb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,8 @@ RUN pnpm run build FROM node:20-alpine AS runner WORKDIR /app +RUN npm install -g pnpm + # 필요한 파일만 복사 COPY --from=builder /app/.next .next COPY --from=builder /app/public public @@ -36,9 +38,7 @@ COPY --from=builder /app/next.config.mjs next.config.mjs # 포트 설정 EXPOSE 3000 ENV NODE_ENV=production -# NEXT_PUBLIC_API_URL은 그대로 유지 ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL -# JWT_SECRET도 원하면 그대로 ENV로 전달 가능 (서버 사이드 로직에서만 접근 가능!) ENV JWT_SECRET=$JWT_SECRET # 실행 From 88044568cba5d819572bb45f4a2f4df24f152de2 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 19:51:17 +0900 Subject: [PATCH 10/19] =?UTF-8?q?chore:=20=EC=BF=A0=ED=82=A4=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - credentials: 'include' 추가 --- app/team/overview/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/team/overview/page.tsx b/app/team/overview/page.tsx index 0a384a5..fa31605 100644 --- a/app/team/overview/page.tsx +++ b/app/team/overview/page.tsx @@ -41,6 +41,7 @@ async function getEvaluationData(organizationId: string): Promise { Authorization: `Bearer ${accessToken}`, }, cache: 'no-store', + credentials: 'include', } ); console.log('accessToken', accessToken); From 28d8f7b452364001364067ffa8ec0406f8f46d37 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 20:17:02 +0900 Subject: [PATCH 11/19] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/actions.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/app/login/actions.ts b/app/login/actions.ts index 5e401c7..97c5064 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -1,8 +1,6 @@ -// loginAction.ts 수정 'use server'; import { redirect } from 'next/navigation'; -import { cookies } from 'next/headers'; export async function loginAction({ userId, password }: { userId: string; password: string }) { const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login`, { @@ -12,19 +10,10 @@ export async function loginAction({ userId, password }: { userId: string; passwo credentials: 'include', }); - const data = await res.json(); - if (!res.ok) { + const data = await res.json(); throw new Error(data.message || '로그인 실패'); } - (await cookies()).set('accessToken', data.accessToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60, - sameSite: 'strict', - path: '/', - }); - redirect('/dashboard'); } From a7750faea1097f50fec6b236e2c05819a9c6cbb1 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 21:08:34 +0900 Subject: [PATCH 12/19] =?UTF-8?q?fix:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 6a1eeb8..7386c34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ RUN pnpm run build FROM node:20-alpine AS runner WORKDIR /app +ARG NEXT_PUBLIC_API_URL +ARG JWT_SECRET + RUN npm install -g pnpm # 필요한 파일만 복사 From 8e3cffd6727226f2213a65c87ccce1c3b5c0c040 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Wed, 25 Jun 2025 22:41:40 +0900 Subject: [PATCH 13/19] =?UTF-8?q?fix:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7386c34..7e9bba5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,8 @@ FROM node:20-alpine AS builder # Build 시 필요한 변수 정의 ARG NEXT_PUBLIC_API_URL ARG JWT_SECRET -# NEXT_PUBLIC_API_URL은 NEXT.js가 정적 페이지 생성 시 사용 + ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL -# JWT_SECRET은 서버 로직에서만 사용할 것이지만, 원하면 그대로 ENV로도 지정 가능 ENV JWT_SECRET=$JWT_SECRET # 작업 디렉토리 @@ -26,9 +25,6 @@ RUN pnpm run build FROM node:20-alpine AS runner WORKDIR /app -ARG NEXT_PUBLIC_API_URL -ARG JWT_SECRET - RUN npm install -g pnpm # 필요한 파일만 복사 @@ -41,7 +37,6 @@ COPY --from=builder /app/next.config.mjs next.config.mjs # 포트 설정 EXPOSE 3000 ENV NODE_ENV=production -ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL ENV JWT_SECRET=$JWT_SECRET # 실행 From 903005e39ed5a9b84f256a0bc4af3998d9f57f9b Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Thu, 26 Jun 2025 09:06:25 +0900 Subject: [PATCH 14/19] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 환경변수 테스트 --- .github/workflows/ci.yml | 2 -- Dockerfile | 7 +++---- app/login/actions.ts | 3 +++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 763b926..e6693c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,6 @@ jobs: run: CI=false npm run build env: NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} # 5. Docker Buildx 설정 - name: Set up Docker Buildx @@ -50,7 +49,6 @@ jobs: tags: ${{ secrets.DOCKERHUB_USERNAME }}/leadme5-front:latest build-args: | NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} - JWT_SECRET=${{ secrets.JWT_SECRET }} # 8. Docker Digest 값 추출 (배포 설정에 사용할 이미지 고유 식별자) - name: Get image digest diff --git a/Dockerfile b/Dockerfile index 7e9bba5..e689c70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,9 @@ # 1️⃣ Build Stage FROM node:20-alpine AS builder -# Build 시 필요한 변수 정의 ARG NEXT_PUBLIC_API_URL -ARG JWT_SECRET ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL -ENV JWT_SECRET=$JWT_SECRET # 작업 디렉토리 WORKDIR /app @@ -25,6 +22,8 @@ RUN pnpm run build FROM node:20-alpine AS runner WORKDIR /app +ARG NEXT_PUBLIC_API_URL + RUN npm install -g pnpm # 필요한 파일만 복사 @@ -37,7 +36,7 @@ COPY --from=builder /app/next.config.mjs next.config.mjs # 포트 설정 EXPOSE 3000 ENV NODE_ENV=production -ENV JWT_SECRET=$JWT_SECRET +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL # 실행 CMD ["pnpm", "start"] diff --git a/app/login/actions.ts b/app/login/actions.ts index 97c5064..51daf95 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -3,6 +3,9 @@ import { redirect } from 'next/navigation'; export async function loginAction({ userId, password }: { userId: string; password: string }) { + console.log('🔍 NEXT_PUBLIC_API_URL:', process.env.NEXT_PUBLIC_API_URL); + console.log('🔍 요청 URL:', `${process.env.NEXT_PUBLIC_API_URL}/login`); + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, From d5140c9951f9c1219bd3989b01f23541ec269e25 Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Thu, 26 Jun 2025 09:26:00 +0900 Subject: [PATCH 15/19] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20ap?= =?UTF-8?q?i=20uri=20=EC=88=98=EC=A0=95=20=ED=9B=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/login/actions.ts b/app/login/actions.ts index 51daf95..b7026ad 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -4,9 +4,9 @@ import { redirect } from 'next/navigation'; export async function loginAction({ userId, password }: { userId: string; password: string }) { console.log('🔍 NEXT_PUBLIC_API_URL:', process.env.NEXT_PUBLIC_API_URL); - console.log('🔍 요청 URL:', `${process.env.NEXT_PUBLIC_API_URL}/login`); + console.log('🔍 요청 URL:', `${process.env.NEXT_PUBLIC_API_URL}/auth/login`); - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login`, { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ employeeNumber: userId, password }), From 1db6468ae6286e4bba24f377329476e6bb531cea Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Thu, 26 Jun 2025 09:40:13 +0900 Subject: [PATCH 16/19] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/actions.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/login/actions.ts b/app/login/actions.ts index b7026ad..870e0f7 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -13,10 +13,17 @@ export async function loginAction({ userId, password }: { userId: string; passwo credentials: 'include', }); + // 🔍 기본 응답 정보 + console.log('🔍 Status:', res.status, res.statusText); + console.log('🔍 Headers:', Object.fromEntries(res.headers.entries())); + + const data = await res.json(); + if (!res.ok) { - const data = await res.json(); throw new Error(data.message || '로그인 실패'); } + console.log('🔍 Success Response:', data); + redirect('/dashboard'); } From 21bdfa556aaa413f805167c0ab7279564160134b Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Thu, 26 Jun 2025 11:08:30 +0900 Subject: [PATCH 17/19] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 클라이언트 컴포넌트에서 수행 --- components/auth/login-form.tsx | 35 ++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index 25f1934..01a0f32 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -12,9 +12,9 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { Eye, EyeOff, LogIn } from 'lucide-react'; import Link from 'next/link'; import { useUserStore } from '@/store/useUserStore'; -import { loginAction } from '@/app/login/actions'; export function LoginForm() { + const router = useRouter(); const setUser = useUserStore((state) => state.setUser); const [formData, setFormData] = useState({ userId: '', @@ -50,14 +50,37 @@ export function LoginForm() { setErrors({}); try { - //loginAction 호출로 변경 - await loginAction({ - userId: formData.userId, - password: formData.password, + // 클라이언트 컴포넌트에서 직접 API 호출 + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ employeeNumber: formData.userId, password: formData.password }), + credentials: 'include', // 쿠키를 자동으로 저장하기 위해 필요 }); - // redirect는 loginAction 내부에서 처리됨 + + // 응답 검사 + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || '로그인 실패'); + } + + // 성공 시 사용자 정보 저장 + const userData = await response.json(); + + // Zustand 스토어에 사용자 정보 저장 + // setUser({ + // id: userData.id || userData.userId, + // name: userData.name, + // role: userData.role || userData.position, + // // 기타 필요한 사용자 정보 + // }); + + // 대시보드로 리다이렉트 + router.push('/dashboard'); + router.refresh(); // 페이지 갱신 (선택적) } catch (error: any) { setErrors({ general: error.message || '로그인 실패' }); + console.error('로그인 오류:', error); } finally { setIsLoading(false); } From 2dc6b9b4b0854d29e8e6938be8eb1bb54dd0737d Mon Sep 17 00:00:00 2001 From: SanghyunLee Date: Sun, 29 Jun 2025 19:54:14 +0900 Subject: [PATCH 18/19] =?UTF-8?q?fix:=20api=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 쿠키의 accessToken 값 꺼내서 api 요청 --- app/team/overview/page.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/team/overview/page.tsx b/app/team/overview/page.tsx index fa31605..c48c073 100644 --- a/app/team/overview/page.tsx +++ b/app/team/overview/page.tsx @@ -1,4 +1,3 @@ -// 수정된 page.tsx import { TeamEvaluation } from '@/components/team/team-evaluation'; import { TeamReport } from '@/components/team/team-report'; import { cookies } from 'next/headers'; @@ -29,11 +28,27 @@ interface PageProps { params: { organizationId: string }; } -async function getEvaluationData(organizationId: string): Promise { +function extractOrganizationIdFromToken(token: string): string | null { + try { + const payloadBase64 = token.split('.')[1]; + const decodedPayload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString()); + return decodedPayload.organizationId?.toString() || null; + } catch (e) { + console.error('토큰 디코딩 실패:', e); + return null; + } +} + +async function getEvaluationData() { try { const cookieStore = await cookies(); const accessToken = cookieStore.get('accessToken')?.value; + if (!accessToken) throw new Error('accessToken 누락'); + + const organizationId = extractOrganizationIdFromToken(accessToken); + if (!organizationId) throw new Error('organizationId 추출 실패'); + const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/quantitative-evaluation/${organizationId}`, { @@ -54,9 +69,8 @@ async function getEvaluationData(organizationId: string): Promise { } } -export default async function Page({ params }: PageProps) { - const { organizationId } = params; - const { evaluated, users } = await getEvaluationData(organizationId); +export default async function Page() { + const { evaluated, users } = await getEvaluationData(); // API 응답 → 컴포넌트 데이터 형식 변환 const teamMembers = users.map((user) => ({ From 2f141b77897b64bc96e14425ff6c52b38456d886 Mon Sep 17 00:00:00 2001 From: soohee Jang Date: Mon, 30 Jun 2025 09:58:26 +0900 Subject: [PATCH 19/19] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/team/overview/page.tsx | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/app/team/overview/page.tsx b/app/team/overview/page.tsx index c48c073..fce51d3 100644 --- a/app/team/overview/page.tsx +++ b/app/team/overview/page.tsx @@ -73,15 +73,25 @@ export default async function Page() { const { evaluated, users } = await getEvaluationData(); // API 응답 → 컴포넌트 데이터 형식 변환 - const teamMembers = users.map((user) => ({ - id: user.userId.toString(), - name: user.name, - role: user.position, - email: user.email, - projects: user.tasks.map((task) => task.name), - performanceScore: user.quarterScore || 0, // null 대체값 - lastEvaluationDate: user.lastUpdated || '', // null 대체값 - })); + const teamMembers = users.map( + (user: { + userId: { toString: () => any }; + name: any; + position: any; + email: any; + tasks: any[]; + quarterScore: any; + lastUpdated: any; + }) => ({ + id: user.userId.toString(), + name: user.name, + role: user.position, + email: user.email, + projects: user.tasks.map((task) => task.name), + performanceScore: user.quarterScore || 0, // null 대체값 + lastEvaluationDate: user.lastUpdated || '', // null 대체값 + }) + ); return (