이번에 인프런에서 온라인 교육을 들으면서 '인프런 머니업 챌린지'에 신청했습니다.
인프런에서 관련 온라인 교육을 결재하고, 챌린지에 신청했습니다.
머니업 챌린지여서 돈까지 벌어야 할거 같은데... 비교적 최근에 온라인 교육을 이수해서 구글 애드센스 신청까지는 하지 못했습니다.
데이터에서 설명하자면 국회에서는 본회의, 각 상임위원회 회의, 국정감사 등 다양한 회의를 진행하고, 그 모든 내용을 회록을 통해 기록하고 있습니다. 이런 다양한 자료를 달력 모양으로 편리하게 확인하고, 세부 내용도 클릭으로 확인하게 하는 웹페이지를 만들고자 습니다.
PRD문서를 작성하는 크게 2가지를 중점적으로 신경썼습니다.
제가 백엔드API로 만들고자 하는 방식은 아래와 같습니다. 간단히 2개만 함수만 만들었습니다.
진짜 Cursor를 활용하면 기획자도 어렵지 않게 수준급 웹사이트를 만들 수 있게 된 것 같습니다.
아래는 제가 실제 사용한 PRD내용입니다. 궁금한 분들은 내용 참고하세요~
# **Project Overview (프로젝트 개요):**
Python FastAPI를 활용해서 프론트엔드 서버를 만들고자 합니다.
달력이 형태의 UI에 각 일자별로 국회에서 진행했던 회의록이 표시되어 있습니다.
http://localhost:8000/MeetingNoteSearch?type=all&yearmonth=202412
type의 종류는 아래와 같습니다.
[all, 국회본회의, 상임위원회,예산결산특별위원회,특별위원회,인사청문회,소위원회,국정감사,국정조사,공청회,청문회,연석회의]
api로 부터 받은 결과는 아래와 같습니다.
```json
[
{
"id": 1,
"conferNum": "054851",
"commName": "국회본회의",
"meeting1": "제423회(2025.03.05-2025.04.03)",
"meeting2": "제2차",
"meetingDate": "2025-03-20",
"daeNum": 22,
"type": "국회본회의"
},
{
"id": 940,
"conferNum": "054857",
"commName": "12.29여객기참사진상규명과피해자및유가족의피해구제를위한특별위원회",
"meeting1": "제423회(2025.03.05-2025.04.03)",
"meeting2": "제4차",
"meetingDate": "2025-03-20",
"daeNum": 22,
"type": "청문회"
}
]
```
결과적으로 출력하길 바라는 디자인은 아래 html 자료를 참고해주세요. 아래 디자인은 지역에 대해서 구분했지만, 본 서비스는 type을 기준으로 구분해주세요.
```php
<style>
body {
font-family: 'Noto Sans KR', sans-serif;
}
.calendar-cell {
height: 120px; /* 높이 조금 증가 */
vertical-align: top;
position: relative;
overflow: hidden; /* 넘치는 내용 숨김 */
width: 14.28%; /* 7일 균등 분할 */
}
.calendar-date {
font-weight: bold;
display: inline-block;
margin-bottom: 4px;
}
.lotto-badge {
display: block;
font-size: 0.75rem; /* 폰트 크기 증가 */
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
padding: 2px 4px; /* 패딩 약간 증가 */
border-radius: 2px;
color: white;
font-weight: 500; /* 약간의 볼드 추가 */
cursor: pointer; /* 포인터 커서 추가 */
}
.lotto-plans-container {
max-height: 70px; /* 일정 최대 높이 제한 */
overflow-y: auto; /* 스크롤 가능하게 설정 */
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
.lotto-plans-container::-webkit-scrollbar {
width: 4px;
}
.lotto-plans-container::-webkit-scrollbar-track {
background: transparent;
}
.lotto-plans-container::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 2px;
}
.lotto-card {
transition: transform 0.2s;
}
.lotto-card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.more-badge {
display: block;
font-size: 0.65rem;
margin-top: 2px;
text-align: center;
padding: 1px 3px;
border-radius: 2px;
background-color: #6B7280;
color: white;
cursor: pointer;
}
/* 테이블 반응형 조정 */
.calendar-table {
table-layout: fixed; /* 테이블 너비 고정 */
width: 100%;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 선택된 카드 하이라이트를 위한 스타일 */
.lotto-card.highlighted {
box-shadow: 0 0 0 2px #3b82f6, 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
</style>
<div class="bg-gray-100 p-6">
<div class="max-w-6xl mx-auto">
<div class="bg-white p-6 rounded-lg shadow-md mb-6">
<h1 class="text-2xl font-bold mb-4">아파트 청약</h1>
<div class="mb-4">
<label for="region" class="block text-lg font-medium mb-2">지역 선택</label>
<select id="region" class="w-full p-2 border border-gray-300 rounded">
<option value="">전체</option>
<option value="서울">서울</option>
<option value="경기">경기</option>
<option value="인천">인천</option>
<option value="부산">부산</option>
<option value="강원">강원</option>
<option value="경남">경남</option>
<option value="경북">경북</option>
<option value="광주">광주</option>
<option value="대구">대구</option>
<option value="대전">대전</option>
<option value="세종">세종</option>
<option value="울산">울산</option>
<option value="전남">전남</option>
<option value="전북">전북</option>
<option value="충남">충남</option>
<option value="충북">충북</option>
<option value="제주">제주</option>
</select>
</div>
<div class="mb-4 flex flex-wrap gap-2" id="region-badges">
<span data-region="" class="inline-block bg-gray-800 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">전체</span>
<span data-region="서울" class="inline-block bg-red-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">서울</span>
<span data-region="경기" class="inline-block bg-green-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">경기</span>
<span data-region="인천" class="inline-block bg-blue-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">인천</span>
<span data-region="부산" class="inline-block bg-orange-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">부산</span>
<span data-region="강원" class="inline-block bg-purple-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">강원</span>
<span data-region="경남" class="inline-block bg-gray-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">경남</span>
<span data-region="경북" class="inline-block bg-teal-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">경북</span>
<span data-region="광주" class="inline-block bg-pink-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">광주</span>
<span data-region="대구" class="inline-block bg-yellow-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">대구</span>
<span data-region="대전" class="inline-block bg-indigo-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">대전</span>
<span data-region="세종" class="inline-block bg-lime-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">세종</span>
<span data-region="울산" class="inline-block bg-amber-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">울산</span>
<span data-region="전남" class="inline-block bg-cyan-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">전남</span>
<span data-region="전북" class="inline-block bg-fuchsia-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">전북</span>
<span data-region="충남" class="inline-block bg-rose-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">충남</span>
<span data-region="충북" class="inline-block bg-sky-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">충북</span>
<span data-region="제주" class="inline-block bg-emerald-500 text-white px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80">제주</span>
</div>
<div class="mb-4" id="calendar-container">
<div class="flex justify-between items-center mb-2">
<h2 class="text-xl font-bold" id="current-month"></h2>
<div class="flex items-center gap-2">
<div class="flex items-center">
<select id="year-select" class="p-1 border border-gray-300 rounded mr-1 text-sm">
<!-- 연도 옵션은 JavaScript로 생성 -->
</select>
<select id="month-select" class="p-1 border border-gray-300 rounded text-sm">
<!-- 월 옵션은 JavaScript로 생성 -->
</select>
</div>
<button id="today-button" class="bg-gray-800 text-white px-3 py-1 rounded text-sm">오늘</button>
<button id="prev-month" class="bg-gray-300 text-gray-800 px-3 py-1 rounded"><</button>
<button id="next-month" class="bg-gray-300 text-gray-800 px-3 py-1 rounded">></button>
</div>
</div>
<table class="w-full text-center border-collapse calendar-table">
<thead>
<tr>
<th class="border p-2 bg-red-50">일</th>
<th class="border p-2">월</th>
<th class="border p-2">화</th>
<th class="border p-2">수</th>
<th class="border p-2">목</th>
<th class="border p-2">금</th>
<th class="border p-2 bg-blue-50">토</th>
</tr>
</thead>
<tbody id="calendar-body">
<!-- 달력 내용은 JavaScript로 생성됩니다 -->
</tbody>
</table>
</div>
</div>
<div id="lotto-plans-container" class="max-w-6xl mx-auto space-y-4">
<!-- 청약 정보는 JavaScript로 생성됩니다 -->
</div>
</div>
</div>
<div id="loading-overlay" class="loading-overlay">
<div class="spinner"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 전역 변수
let currentDate = new Date();
let currentYear = currentDate.getFullYear();
let currentMonth = currentDate.getMonth();
let allLottoPlans = [];
let filteredLottoPlans = [];
// 지역별 색상 매핑
const regionColors = {
'서울': 'bg-red-500',
'경기': 'bg-green-500',
'인천': 'bg-blue-500',
'부산': 'bg-orange-500',
'강원': 'bg-purple-500',
'경남': 'bg-gray-500',
'경북': 'bg-teal-500',
'광주': 'bg-pink-500',
'대구': 'bg-yellow-500',
'대전': 'bg-indigo-500',
'세종': 'bg-lime-500',
'울산': 'bg-amber-500',
'전남': 'bg-cyan-500',
'전북': 'bg-fuchsia-500',
'충남': 'bg-rose-500',
'충북': 'bg-sky-500',
'제주': 'bg-emerald-500'
};
// 현재 선택된 지역
let selectedRegion = '';
// 요소 참조
const regionSelect = document.getElementById('region');
const calendarBody = document.getElementById('calendar-body');
const currentMonthElement = document.getElementById('current-month');
const prevMonthButton = document.getElementById('prev-month');
const nextMonthButton = document.getElementById('next-month');
const todayButton = document.getElementById('today-button');
const lottoPlansContainer = document.getElementById('lotto-plans-container');
const loadingOverlay = document.getElementById('loading-overlay');
const regionBadges = document.getElementById('region-badges').querySelectorAll('span');
const yearSelect = document.getElementById('year-select');
const monthSelect = document.getElementById('month-select');
// 지역 배지에 클릭 이벤트 추가
regionBadges.forEach(badge => {
badge.addEventListener('click', function() {
const region = this.getAttribute('data-region');
// select 요소의 값 변경
regionSelect.value = region;
// 선택된 지역 시각적으로 표시
regionBadges.forEach(b => {
b.classList.remove('ring-2', 'ring-offset-2');
});
this.classList.add('ring-2', 'ring-offset-2');
// 선택된 지역으로 필터링 실행
selectedRegion = region;
filterLottoPlans();
renderCalendar();
renderLottoPlans();
});
});
// 기존 코드에서 regionSelect의 change 이벤트 핸들러 수정
regionSelect.addEventListener('change', function() {
selectedRegion = this.value;
// 선택된 지역에 맞는 배지 강조
regionBadges.forEach(badge => {
const badgeRegion = badge.getAttribute('data-region');
badge.classList.remove('ring-2', 'ring-offset-2');
if (badgeRegion === selectedRegion) {
badge.classList.add('ring-2', 'ring-offset-2');
}
});
filterLottoPlans();
renderCalendar();
renderLottoPlans();
});
// 연도, 월 선택 드롭다운 초기화 함수 수정
function initDateSelectors() {
// 연도/월 선택 드롭다운 초기화 전에 기존 내용 비우기
yearSelect.innerHTML = '';
monthSelect.innerHTML = '';
// 연도 옵션 (현재 연도 기준 ±10년)
const today = new Date();
const systemYear = today.getFullYear();
for (let year = systemYear - 10; year <= systemYear + 10; year++) {
const option = document.createElement('option');
option.value = year;
option.textContent = `${year}년`;
// 현재 설정된 연도에 맞게 선택
if (year === currentYear) {
option.selected = true;
}
yearSelect.appendChild(option);
}
// 월 옵션
const monthNames = ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"];
for (let month = 0; month < 12; month++) {
const option = document.createElement('option');
option.value = month;
option.textContent = monthNames[month];
// 현재 설정된 월에 맞게 선택
if (month === currentMonth) {
option.selected = true;
}
monthSelect.appendChild(option);
}
}
// 연도, 월 변경 이벤트 핸들러 수정
yearSelect.addEventListener('change', function() {
currentYear = parseInt(this.value);
updateDateDisplay();
renderCalendar();
renderLottoPlans();
});
monthSelect.addEventListener('change', function() {
currentMonth = parseInt(this.value);
updateDateDisplay();
renderCalendar();
renderLottoPlans();
});
// 날짜 표시 업데이트 함수 추가
function updateDateDisplay() {
// monthNames 변수 정의 추가
const monthNames = ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"];
currentMonthElement.textContent = `${currentYear}년 ${monthNames[currentMonth]}`;
// select 요소의 값도 동기화
if (yearSelect && monthSelect) {
yearSelect.value = currentYear;
monthSelect.value = currentMonth;
}
}
// 이전/다음 월 버튼 이벤트 핸들러 수정
prevMonthButton.addEventListener('click', function() {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
updateDateDisplay();
renderCalendar();
renderLottoPlans();
});
nextMonthButton.addEventListener('click', function() {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
updateDateDisplay();
renderCalendar();
renderLottoPlans();
});
// 오늘 버튼 이벤트 핸들러 수정
todayButton.addEventListener('click', function() {
const today = new Date();
currentYear = today.getFullYear();
currentMonth = today.getMonth();
updateDateDisplay();
renderCalendar();
renderLottoPlans();
});
// 청약 정보 필터링 함수
function filterLottoPlans() {
if (selectedRegion) {
filteredLottoPlans = allLottoPlans.filter(plan => plan.region === selectedRegion);
} else {
filteredLottoPlans = [...allLottoPlans];
}
}
```
기존 FastAPI 백엔드가 8000번 포트에서 운영중이기 때문에 8001번 포트로 실행해야 합니다.