
프로젝트명: Ideal Type World Cup & Quiz Game Platform
기간: 2026.01.29 ~ 2026.02.08
팀: 한화 SW Camp 22기 3조
작성자: gusgh075
| 분류 | 기술 |
|---|---|
| 프론트엔드 프레임워크 | Vue.js 3.5.24, Vite 5.4.11 |
| 라우팅 | Vue Router 5.0.1 |
| 상태 관리 | Pinia 3.0.4 |
| UI 라이브러리 | Element Plus 2.13.2 |
| HTTP 통신 | Axios 1.13.4 |
| 백엔드 (Mock) | JSON Server 1.0, json-server-auth (JWT 인증) |
| 기타 | UUID, html2canvas, Galmuri Font (픽셀 한글 폰트) |
feat/, fix/, hotfix/), PR 단위로 머지하는 워크플로우를 철저히 지켰다.월드컵 멀티파일 업로드 기능 (PR #21)
파일 업로드 확장자 제한 (PR #5)
jpg, jpeg, png, gif, webp 확장자 필터링 로직을 추가하여 보안성과 안정성을 강화하였다.월드컵 8강/16강/32강/64강 라운드 시스템 (PR #42, #52)
조회수/플레이수 집계 로직 (PR #75)
db.json에서 해당 데이터를 자동으로 카운트하는 집계 로직을 구현하였다.월드컵 랭킹 페이지 및 승률 계산 (PR #77)
help.js에 승률 계산 유틸리티 함수를 정의하였다.이미지 URL 관리 체계 구축
.env 파일에서 json-server 주소를 통합 관리하고, getImageURL 헬퍼 함수를 구현하여 프로젝트 전반의 이미지 경로 처리를 일원화하였다.vite.config.js에서 /api 프록시를 localhost:3000으로 설정하여 개발 환경의 API 통신을 안정화하였다.// stores/worldcup.js (수정 전)
const getRoundName = () => {
const remainingCount = currentRound.value.length;
// 잘못된 조건문: 64강인데 결승으로 표시됨
if (remainingCount === 2) return '결승';
if (remainingCount <= 4) return '4강';
if (remainingCount <= 8) return '8강';
// ... 이하 생략
}
// stores/worldcup.js (수정 후)
const getRoundName = () => {
const remainingCount = currentRound.value.length;
// 정확한 조건문 순서와 비교 연산자 수정
if (remainingCount === 64) return '64강';
if (remainingCount === 32) return '32강';
if (remainingCount === 16) return '16강';
if (remainingCount === 8) return '8강';
if (remainingCount === 4) return '4강';
if (remainingCount === 2) return '결승';
return '게임 종료';
}
<= 비교 연산자 대신 === 정확한 일치 비교 사용<!-- views/worldcup/WorldcupGame.vue (수정 전) -->
<template>
<div class="candidate-card">
<img :src="`/uploads/${candidate.image}`" />
</div>
</template>
<!-- views/worldcup/WorldcupGame.vue (수정 후) -->
<script setup>
import { getImageURL } from '@/utils/helper.js'
</script>
<template>
<div class="candidate-card">
<img :src="getImageURL(candidate.image)" />
</div>
</template>
// utils/helper.js (새로 추가)
export const getImageURL = (filename) => {
if (!filename) return '/default-avatar.png';
// 환경변수 기반 통합 URL 관리
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
return `${baseURL}/uploads/${filename}`;
}
.env) 기반의 통합 이미지 URL 관리 체계 도입<!-- views/worldcup/WorldcupList.vue (수정 전) -->
<img :src="worldcup.thumbnail" />
<!-- views/worldcup/WorldcupResult.vue (수정 전) -->
<img :src="`http://localhost:3000${winner.image}`" />
<!-- views/worldcup/WorldcupList.vue (수정 후) -->
<script setup>
import { getImageURL } from '@/utils/helper.js'
</script>
<img :src="getImageURL(worldcup.thumbnail)" />
<!-- views/worldcup/WorldcupResult.vue (수정 후) -->
<img :src="getImageURL(winner.image)" />
// .env (환경변수 설정)
VITE_API_BASE_URL=http://localhost:3000
// vite.config.js (프록시 설정 추가)
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
return {
server: {
proxy: {
'/api': {
target: env.VITE_API_BASE_URL || 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
})
getImageURL() 헬퍼 함수 통일 사용.env 파일로 API 주소 중앙 관리identity.js 파일이 누락되어 런타임 에러 발생// utils/index.js (수정 전)
export * from './validators'
// identity 모듈 누락
// utils/identity.js (새로 추가)
export const identity = (value) => value;
export const generateId = () => {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// utils/index.js (수정 후)
export * from './validators'
export * from './identity' // 명시적 추가
export * from './storage'
export * from './helper'
<!-- views/create/WorldcupCreate.vue (수정 전) -->
<script setup>
const submitWorldcup = async () => {
try {
await worldcupApi.create(formData);
router.push('/worldcup');
// 에러 처리 없음
} catch (error) {
console.error(error); // 콘솔에만 출력
}
}
</script>
<!-- views/create/WorldcupCreate.vue (수정 후) -->
<script setup>
import { ElMessage } from 'element-plus'
const submitWorldcup = async () => {
// 유효성 검증
if (!formData.title || !formData.description) {
ElMessage.error('제목과 설명을 모두 입력해주세요.');
return;
}
if (candidates.length < 32) {
ElMessage.error('최소 32명의 후보를 등록해야 합니다.');
return;
}
try {
await worldcupApi.create(formData);
ElMessage.success('월드컵이 생성되었습니다!');
router.push('/worldcup');
} catch (error) {
console.error(error);
ElMessage.error(error.response?.data?.message || '월드컵 생성에 실패했습니다.');
}
}
</script>
ElMessage 컴포넌트를 활용한 사용자 친화적 피드백npm install 실패// package.json (수정 전)
{
"dependencies": {
"vite": "^5.0.0",
"vue": "^3.3.0"
}
}
// package.json (수정 후)
{
"dependencies": {
"vite": "^5.4.11",
"vue": "^3.5.24",
"vue-router": "^5.0.1"
}
}
# 의존성 재설치 명령어
rm -rf node_modules package-lock.json
npm install
package-lock.json 재생성으로 의존성 트리 정리| 트러블슈팅 항목 | 핵심 배운 점 |
|---|---|
| 라운드 버그 | 조건문 순서와 비교 연산자의 중요성 (=== vs <=) |
| 이미지 경로 | 환경변수 기반 중앙 집중식 설정 패턴 |
| 모듈 누락 | 명시적 export와 빌드 번들링 이해 |
| 에러 UX | 사용자 피드백의 중요성과 UI 라이브러리 활용 |
| 의존성 관리 | npm 패키지 버전 호환성 체크의 필요성 |
이번 프로젝트는 팀원간 협업을 제대로 경험해볼 수 있었던 것 같다. 기술적 이해도와 별개로 모두가 적극적으로 참여했고, 다양한 의견을 냈다. 또한 다들 새벽까지 프로젝트를 진행하는 피곤한 스케쥴임이였음에도 불구하고 긍정적인 마인드로 진행함에 있어 깊은 감사를 표하고 싶다. 앞으로도 서로의 열정을 불태워 모두가 만족할만한 결과물을 내는 경험을 많이 하고 싶다.
다들 너무 수고 많았고, 자주자주 봤으면 좋겠다! 곧 다가오는 최종프로젝트도 다같이 화이팅이다!
우우우우우ㅜ 내꺼 따라쟁이 우우우우우우