diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e6693c5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,80 @@
+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
+ env:
+ NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
+
+ # 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
+ build-args: |
+ NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}
+
+ # 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
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e689c70
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,42 @@
+# 1️⃣ Build Stage
+FROM node:20-alpine AS builder
+
+ARG NEXT_PUBLIC_API_URL
+
+ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
+
+# 작업 디렉토리
+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
+
+ARG NEXT_PUBLIC_API_URL
+
+RUN npm install -g pnpm
+
+# 필요한 파일만 복사
+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.mjs next.config.mjs
+
+# 포트 설정
+EXPOSE 3000
+ENV NODE_ENV=production
+ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
+
+# 실행
+CMD ["pnpm", "start"]
diff --git a/app/login/actions.ts b/app/login/actions.ts
new file mode 100644
index 0000000..870e0f7
--- /dev/null
+++ b/app/login/actions.ts
@@ -0,0 +1,29 @@
+'use server';
+
+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}/auth/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 }),
+ 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) {
+ throw new Error(data.message || '로그인 실패');
+ }
+
+ console.log('🔍 Success Response:', data);
+
+ redirect('/dashboard');
+}
diff --git a/app/team/overview/page.tsx b/app/team/overview/page.tsx
index 0e02693..fce51d3 100644
--- a/app/team/overview/page.tsx
+++ b/app/team/overview/page.tsx
@@ -1,19 +1,105 @@
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[];
+}
+
+interface PageProps {
+ params: { organizationId: string };
+}
- // 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
+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;
+ }
}
-export default async function TeamOverviewPage() {
- // const hasBeenEvaluated = await getTeamEvaluationStatus();
- const hasBeenEvaluated = true;
+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}`,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ cache: 'no-store',
+ credentials: 'include',
+ }
+ );
+ 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() {
+ const { evaluated, users } = await getEvaluationData();
+
+ // API 응답 → 컴포넌트 데이터 형식 변환
+ 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
{hasBeenEvaluated ? : }
;
+ return (
+
+ {evaluated ? (
+
+ ) : (
+
+ )}
+
+ );
}
diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx
index bc89853..01a0f32 100644
--- a/components/auth/login-form.tsx
+++ b/components/auth/login-form.tsx
@@ -14,8 +14,8 @@ import Link from 'next/link';
import { useUserStore } from '@/store/useUserStore';
export function LoginForm() {
- const setUser = useUserStore((state) => state.setUser);
const router = useRouter();
+ const setUser = useUserStore((state) => state.setUser);
const [formData, setFormData] = useState({
userId: '',
password: '',
@@ -50,33 +50,37 @@ export function LoginForm() {
setErrors({});
try {
- const res = await fetch('/api/login', {
+ // 클라이언트 컴포넌트에서 직접 API 호출
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- userId: formData.userId,
- password: formData.password,
- }),
- credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ employeeNumber: formData.userId, password: formData.password }),
+ credentials: 'include', // 쿠키를 자동으로 저장하기 위해 필요
});
- const data = await res.json();
-
- if (!res.ok) {
- setErrors({ general: data.error || '로그인 실패' });
- return;
+ // 응답 검사
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || '로그인 실패');
}
- const user = data.user;
- const accessToken = data.accessToken;
+ // 성공 시 사용자 정보 저장
+ const userData = await response.json();
+
+ // Zustand 스토어에 사용자 정보 저장
+ // setUser({
+ // id: userData.id || userData.userId,
+ // name: userData.name,
+ // role: userData.role || userData.position,
+ // // 기타 필요한 사용자 정보
+ // });
- setUser(user, accessToken);
- localStorage.setItem('user', JSON.stringify(user));
+ // 대시보드로 리다이렉트
router.push('/dashboard');
- } catch (error) {
- setErrors({ general: '서버 연결 오류' });
+ router.refresh(); // 페이지 갱신 (선택적)
+ } catch (error: any) {
+ setErrors({ general: error.message || '로그인 실패' });
+ console.error('로그인 오류:', error);
} 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 */}
정렬:
-