[인프런] 나의 인프런 머니업 챌린지 도전기

2025. 3. 24. 23:51·온라인강의

 

공부기간 : 3월 22일~3월 23일

 

신청한 이유:  

1) CUSOR와 FastAPI를 통해 쉽게 백엔드 API 서버를 만드는 연습

2) 우수 신청자는 교육비를 환급해준다고 해서~ㅋㅋ  

 

이번에 인프런에서 온라인 교육을 들으면서 '인프런 머니업 챌린지'에 신청했습니다.

 

인프런에서 관련 온라인 교육을 결재하고, 챌린지에 신청했습니다.

 

지난 번에 신청한 온라인 교육은 [개발부터 수익화까지] AI로 코드 한 줄 짜지 않고 만드는 IT 올인원 실전 프로젝트! 였고요~ 아래는 제가 작성한 교육 관련 작성한 블로그입니다.

 

https://kyuhyunkang.tistory.com/2

 

머니업 챌린지여서 돈까지 벌어야 할거 같은데... 비교적 최근에 온라인 교육을 이수해서 구글 애드센스 신청까지는 하지 못했습니다. 

 

 

FastAPI로 작성한 최종 웹페이지
국회회의록 백엔드 API서버

 

온라인 교육을 들으면서 여러 노하우를 파악해서 어설프게 코드부터 만들지 않고, 꼼꼼하게 PRD문서를 만들고, 이를 통해 Cursor를 통해 한번에 코드 작성을 요청하는 방식으로 효율적?으로 작업을 수행했습니다. 

 

데이터는 data.go.kr에서 제가 관심있는 '국회 회의록' 데이터를 사용했습니다. 

 

https://www.data.go.kr/data/3057576/openapi.do

 

국회 국회사무처_회의록 정보

(국회입법정보)제헌 이후 현재까지 모든 공개회의의 텍스트 회의록

www.data.go.kr

 

데이터에서 설명하자면 국회에서는  본회의, 각 상임위원회 회의, 국정감사 등 다양한 회의를 진행하고, 그 모든 내용을 회록을 통해 기록하고 있습니다. 이런 다양한 자료를 달력 모양으로 편리하게 확인하고, 세부 내용도 클릭으로 확인하게 하는 웹페이지를 만들고자 습니다. 

 

PRD문서를 작성하는 크게 2가지를 중점적으로 신경썼습니다.

1. 제공하는 'API 활용 가이드' 내용을 비교적 상세하게 포함킴(요청 메시지 명세, 응답 메시지 명세, 요청 / 응답 메시지 예제)

2. DB구조를 명확히 해서 변수 명을 명확히 함(이전 프로젝트 시, cursor로 작업하는데 AI가 변수명을 혼동하여 많은 시간이 소요되었습니다. ㅠㅜ)

 

제가 백엔드API로 만들고자 하는 방식은 아래와 같습니다. 간단히 2개만 함수만 만들었습니다.

1. /MeetingNoteSave (Post방식) :  data.go.kr의 국회회의록 데이터를 API로 받아서 db(mysql)에 저장합니다. 
2. /MeetingNoteSearch (Get방식) : /MeetingNoteSave으로 저장된 DB의 정보를 사용자의 GET요청으로 데이터를 제공합니다. 

 

프론트엔드 웹 페이지는 이전 '부동산앱의 디자인된 웹페이지 html 파일'을 함께 PRD문서에 넣어주었더니 한번에 페이지를 생성해주었습니다. PRD문서를 작성한 15분 포함하여 총 20분 만에 웹페이지를 만들었습니다.  

 

진짜 Cursor를 활용하면 기획자도 어렵지 않게 수준급 웹사이트를 만들 수 있게 된 것 같습니다.

 

아래는 제가 실제 사용한 PRD내용입니다. 궁금한 분들은 내용 참고하세요~

 

----

백엔드API PRD 예시

# **Project Overview (프로젝트 개요):**

Python FastAPI를 활용해서 백엔드 API서버를 만들고자 합니다.  

API는 2개를 만들고자 합니다.

1. /MeetingNoteSave (Post방식) :  data.go.kr의 국회회의록 데이터를 API로 받아서 db(mysql)에 저장합니다.
2. /MeetingNoteSearch (Get방식) : /MeetingNoteSave으로 저장된 DB의 정보를 사용자의 GET요청으로 데이터를 제공합니다.

# **Core Functionalities (핵심 기능):**

## 1. MeetingNoteSave (Post방식) :

국회 회의록 데이터 요청 코드 예시)

### ① 요청 메시지 명세

| 항목명(영문) | 항목명(국문) | 항목크기 | 항목구분 | 샘플데이터 | 항목설명 |
| --- | --- | --- | --- | --- | --- |
| numOfRows | 한 페이지 결과 수 | 4 | 0 | 10 | 한 페이지 결과 수 |
| pageNo | 페이지 번호 | 4 | 0 | 1 | 페이지 번호 |
| dae_num | 대수 | 20 | 0 | 19 | 대수 |
| class_code | 회의종류코드 | 20 | 0 | 3 | 회의종류코드
  1.국회본회의 1
2.전원위원회 7
3.상임위원회 2
4.예산결산특별위원회 4
5.특별위원회 3
6.인사청문회 9
7.소위원회 8
8.국정감사 5
9.국정조사 6
10.공청회 10
11.청문회 11
12.연석회의 12
각 파라미터들 중복 사용 불가
 |

※ 항목구분 : 필수(1), 옵션(0), 1건 이상 복수건(1..n), 0건 또는 복수건(0..n)

### ② 응답 메시지 명세

| 항목명(영문) |  | 항목명(국문) | 항목크기 | 항목구분 | 샘플데이터 | 항목설명 |
| --- | --- | --- | --- | --- | --- | --- |
| Item |  |  |  |  |  |  |
|  | conferNum | 회의번호 | 20 | 1 | 043382 | 회의번호 |
|  | commName | 위원회 | 20 | 1 | 감사원장(황찬현)임명동의에관한인사청문특별위원회 | 위원회 |
|  | gbn | 정기국회구분 | 20 | 0 | (정기회) | 매년 9월에 100일간 열리는 정기국회 표시 |
|  | gbn1 | 국정감사구분 | 20 | 0 | 01 | 국정감사가 아닌 경우 01 표시 |
|  | meeting1 | 회기별 | 100 | 1 | 제320회(2013.09.02-2013.12.10) | 회기별 |
|  | meeting2 | 차수별 | 100 | 1 | 제4차(2013년월11일) | 차수별 |
|  | appendixPdfLink1 | 부록 | 1000 | 0 | - | 부록 |
|  | appendixPdfLink2 | 보존부록 | 1000 | 0 | - | 보존부록 |
|  | hwpLink | 회의록
HWP파일 경로 | 100 | 0 | http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=043382&fileType=hwp | 회의록
HWP파일  경로 |
|  | pdfLink | 회의록
PDF파일 경로 | 200 | 0 | http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=043382&fileType=PDF | 회의록
PDF파일 경로 |
|  | vodLink | 영상 회의록
VOD 경로 | 100 | 0 | http://w3.assembly.go.kr/jsp/vod/vod.do?cmd=vod&mc=4EW&ct1=19&ct2=320&ct3=04 | 영상 회의록
VOD 경로 |
|  | Summary | 요약정보
링크 경로 | 20 | 0 | http://likms.assembly.go.kr/record/mhs-10-030.do?conferNum=043382 | 요약정보는
 
요약정보(getSummaryInfoList)에서
 
subName참조
 
요약정보(출석자)( getSummaryAttenInfoList)
에서
 
attOrg
attName
참조 |

※ 항목구분 : 필수(1), 옵션(0), 1건 이상 복수건(1..n), 0건 또는 복수건(0..n)

### ③ 요청 / 응답 메시지 예제

REST(URI)

---

[http://apis.data.go.kr/9710000/ProceedingInfoService/getAllConInfoList?class_code=3&dae_num=19&ServiceKey](http://openapi.assembly.go.kr/openapi/service/ProceedingInfoService/getAllConInfoList?class_code=3&dae_num=19&ServiceKey)=[서비스키]

---

서비스키는 .env 파일의 ‘API_DencodingKey’에 대한 값입니다.

응답 메시지

---

<?xml version="1.0" encoding="UTF-8" standalone="true"?>

- <response>
- <header>

<resultCode>00</resultCode>

<resultMsg>NORMAL SERVICE.</resultMsg>

</header>

- <body>
- <items>
- <item>

<commName>감사원장(황찬현)임명동의에관한인사청문특별위원회</commName>

<conferNum>043382</conferNum>

<gbn>(정기회)</gbn>

<gbn1>01</gbn1>

<hwpLink>http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=043382&fileType=hwp</hwpLink>

<meeting1>제320회(2013.09.02-2013.12.10)</meeting1>

<meeting2>제4차(2013년1월11일)</meeting2>

<pdfLink>http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=043382&fileType=PDF</pdfLink>

<summary>http://likms.assembly.go.kr/record/mhs-10-030.do?conferNum=043382</summary>

<vodLink>http://w3.assembly.go.kr/jsp/vod/vod.do?cmd=vod&mc=4EW&ct1=19&ct2=320&ct3=04

</item>

(생략)

- <item>

<commName>공무원연금개혁특별위원회</commName>

<conferNum>045045</conferNum>

<gbn> </gbn>

<gbn1>01</gbn1>

<hwpLink>http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=045045&fileType=hwp</hwpLink>

<meeting1>제331회(2015.02.02-2015.03.03)</meeting1>

<meeting2>제7차(2015년2월04일)</meeting2>

<pdfLink>http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=045045&fileType=PDF</pdfLink>

<summary>http://likms.assembly.go.kr/record/mhs-10-030.do?conferNum=045045</summary>

<vodLink>http://w3.assembly.go.kr/jsp/vod/vod.do?cmd=vod&mc=4FJ&ct1=19&ct2=331&ct3=07</vodLink>

</item>

</items>

<numOfRows>10</numOfRows>

<pageNo>1</pageNo>

<totalCount>349</totalCount>

</body>

</response>

---

특이한 사항은

파라미터에 classcode를 1,7,2,4,3,9.8,5,6,10,11,12를 각각 넣고 조회해서 해당 데이터(국회본회의, 상임위원회….)를 db에 저장해야 합니다.

### ③ DB 구조

|  | 회의록 API | DB 저장 |
| --- | --- | --- |
|  | conferNum | conferNum |
|  | commName | commName |
|  | gbn |  |
|  | gbn1 |  |
|  | hwpLink | hwpLink |
|  | meeting1 | meeting1 |
|  | meeting2 | meeting2 |
|  |  | meetingDate |
|  | pdfLink | pdfLink |
|  | vodLink | vodLink |
|  | summary | summary |
|  |  | type |
|  |  | daeNum |

받는 데이터 중 gbn, gbn1은 제외하고, type은 classcode를 1,7,2,4,3,9.8,5,6,10,11,12를 각각 넣고 조회되었을 때, 그 회의록 종류가 저장됩니다.

예를 들어서 type은 (classcode가 1로 api조회를 했을 경우, ‘국회본회의’가 저장되고, classcode가 2로 api조회를 했을 경우 ‘상임위원회’가 저장됩니다.

또한 요청 URL에 dae_num=22의 결과인 경우 DB에 daeNum컬럼의 22값으로 저장됩니다.

회의록 api의 meeting2의 괄호 안의 날짜 데이터를 DB의 meetingDate컬럼의 값으로 저장합니다. 예를들어 meeting2가 ‘제7차(2015년2월04일)’일 경우, meetingDate 에 2015-02-04 (날짜)데이터로 입력이 들어갑니다.

DB의 구조는 아래와 같습니다.

**CREATE** **TABLE** CommitteeMeetings (

id **INT** **PRIMARY** **KEY** **AUTO_INCREMENT**,

commName **VARCHAR**(255) **NOT** **NULL**,

conferNum **VARCHAR**(50) **NOT** **NULL** **UNIQUE**,

hwpLink **TEXT**,

meeting1 **VARCHAR**(255),

meeting2 **VARCHAR**(255),

meetingDate **DATE**,

daeNum **INT**,

pdfLink **TEXT**,

summary **TEXT**,

vodLink **TEXT**,

**type** **VARCHAR**(255)

);

## 2. MeetingNoteSearch (Get방식) :

DB 접속정보는 .env파일에 있습니다.

type(회의록 종류)

날짜(연월)YYYYmm 형식

daeNum(국회 대수)

3가지를 파라미터로 넣고 이에 대한 결과물을 출력하는 api를 만듭니다.

# **Current File Structure (현재 파일 구조):**

실제 프로젝트가 생성되면 커서를 이용해 작성할 예정이므로 현재는 비워둡니다.

기존 FastAPI 백엔드가 8000번 포트에서 운영중이기 때문에 8001번 포트로 실행해야 합니다.

 

프론트엔드 웹페이지 PRD 예시

# **Project Overview (프로젝트 개요):**

Python FastAPI를 활용해서 프론트엔드 서버를 만들고자 합니다.  

달력이 형태의 UI에 각 일자별로 국회에서 진행했던 회의록이 표시되어 있습니다.

http://localhost:8000/MeetingNoteSearch?type=all&yearmonth=202412

type의 종류는 아래와 같습니다.

[all, 국회본회의, 상임위원회,예산결산특별위원회,특별위원회,인사청문회,소위원회,국정감사,국정조사,공청회,청문회,연석회의]

api로 부터 받은 결과는 아래와 같습니다.

```json
[
  {
    "id": 1,
    "conferNum": "054851",
    "commName": "국회본회의",
    "hwpLink": "http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=054851&fileType=hwp",
    "meeting1": "제423회(2025.03.05-2025.04.03)",
    "meeting2": "제2차",
    "meetingDate": "2025-03-20",
    "daeNum": 22,
    "pdfLink": "http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=054851&fileType=PDF",
    "summary": "http://likms.assembly.go.kr/record/mhs-10-030.do?conferNum=054851",
    "vodLink": "http://w3.assembly.go.kr/jsp/vod/vod.do?cmd=vod&mc=10&ct1=22&ct2=423&ct3=02",
    "type": "국회본회의"
  },
  {
    "id": 940,
    "conferNum": "054857",
    "commName": "12.29여객기참사진상규명과피해자및유가족의피해구제를위한특별위원회",
    "hwpLink": "http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=054857&fileType=hwp",
    "meeting1": "제423회(2025.03.05-2025.04.03)",
    "meeting2": "제4차",
    "meetingDate": "2025-03-20",
    "daeNum": 22,
    "pdfLink": "http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=054857&fileType=PDF",
    "summary": "http://likms.assembly.go.kr/record/mhs-10-030.do?conferNum=054857",
    "vodLink": "http://w3.assembly.go.kr/jsp/vod/vod.do?cmd=vod&mc=4IG&ct1=22&ct2=423&ct3=04",
    "type": "청문회"
  }
]
```

결과적으로 출력하길 바라는 디자인은 아래 html 자료를 참고해주세요. 아래 디자인은 지역에 대해서 구분했지만, 본 서비스는 type을 기준으로 구분해주세요.

```php

<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet">
<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">&lt;</button>
                        <button id="next-month" class="bg-gray-300 text-gray-800 px-3 py-1 rounded">&gt;</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];
        }
    }
```

디자인은 [tailwinds.css](https://tailwindcss.com/)로 해주시고, 아래 출력되는 회의록 리스트는 이모지를 통해 예쁘게 정리되도록 해주세요.

기존 FastAPI 백엔드가 8000번 포트에서 운영중이기 때문에 8001번 포트로 실행해야 합니다.

 

 

'온라인강의' 카테고리의 다른 글

[인프런] Flutter로 만드는 LLM 챗봇 (feat. Gemini)  (0) 2025.04.05
[인프런] Firebase보다 10배 좋은 Supabase  (0) 2025.04.02
[인프런] 인프런 지식공유 챌린지 1회차 참석  (0) 2025.04.01
[인프런] 시작해보세요! 당신의 첫 지식 공유  (0) 2025.03.26
[인프런] [개발부터 수익화까지] AI로 코드 한 줄 짜지 않고 만드는 IT 올인원 실전 프로젝트!  (0) 2025.03.23
'온라인강의' 카테고리의 다른 글
  • [인프런] Firebase보다 10배 좋은 Supabase
  • [인프런] 인프런 지식공유 챌린지 1회차 참석
  • [인프런] 시작해보세요! 당신의 첫 지식 공유
  • [인프런] [개발부터 수익화까지] AI로 코드 한 줄 짜지 않고 만드는 IT 올인원 실전 프로젝트!
AI강선생
AI강선생
AI강선생의 블로그 입니다.
  • AI강선생
    나의 배움과 성장의 궤적
    AI강선생
  • 전체
    오늘
    어제
    • 분류 전체보기 (59)
      • 온라인강의 (45)
      • 오프라인강의 (2)
      • 독서 (1)
      • 생각과다짐 (6)
      • 도메인 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    인프런
    llmagent
    docker
    LangChain
    PostgreSQL
    FastAPI
    클로드코드
    길벗
    챌린지
    한빛미디어
    Claude
    Redis
    AI시대
    스프링부트
    cursor
    java
    이지스퍼블리싱
    오레일리
    spring
    티스토리
    국회
    rustfs
    AI agent
    Python
    에이전트
    랭체인
    유리링
    혼공바이브코딩
    게임기획
    claude code
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
AI강선생
[인프런] 나의 인프런 머니업 챌린지 도전기
상단으로

티스토리툴바