<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>랩보다 AI 더 잘해지기</title>
    <link>https://laycoder.tistory.com/</link>
    <description>JASN OK / 제이슨 옥
RAPPER / AI Programmer</description>
    <language>ko</language>
    <pubDate>Tue, 11 Aug 2026 13:12:09 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>랩보다 AI 더 잘해지기</managingEditor>
    <image>
      <title>랩보다 AI 더 잘해지기</title>
      <url>https://tistory1.daumcdn.net/tistory/3864251/attach/a3a1f77fc5934edf87aeb00d3360452e</url>
      <link>https://laycoder.tistory.com</link>
    </image>
    <item>
      <title>미니프로젝트 1일차｜공공임대 청약 통합 안내 서비스 개발과 오류 해결</title>
      <link>https://laycoder.tistory.com/249</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 미니프로젝트로 여러 기관에 흩어져 있는 공공임대 청약 공고를 한곳에서 확인할 수 있는 서비스를 개발하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서비스는 사이트에서 직접 청약을 신청하는 것이 아니라, 사용자가 지역&amp;middot;보증금&amp;middot;월세 등의 조건으로 공고를 검색하고 원문 사이트로 이동할 수 있도록 돕는 청약 통합 안내 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 1일차에는 공고 등록&amp;middot;조회&amp;middot;수정&amp;middot;삭제부터 이미지 관리, 정렬, 페이지네이션까지 다양한 기능을 개발했다. 특히 기능 구현보다 실제 데이터를 안전하게 다루는 과정에서 많은 것을 배울 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 기술 스택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분사용 기술&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;백엔드&lt;/td&gt;
&lt;td&gt;FastAPI, Pydantic v2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터베이스&lt;/td&gt;
&lt;td&gt;Supabase PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이미지 저장소&lt;/td&gt;
&lt;td&gt;Supabase Storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관리자 화면&lt;/td&gt;
&lt;td&gt;Streamlit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자 화면&lt;/td&gt;
&lt;td&gt;Streamlit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;협업&lt;/td&gt;
&lt;td&gt;GitHub Issue &amp;rarr; PR &amp;rarr; develop 병합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 하나의 FastAPI 백엔드에 관리자용과 사용자용 Streamlit 앱이 각각 연결되는 구조다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FastAPI 백엔드 (8000)
├── 관리자 화면 (8501) : 공고 등록&amp;middot;수정&amp;middot;삭제
└── 사용자 화면 (8502) : 공고 조회&amp;middot;즐겨찾기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자와 일반 사용자가 사용하는 기능이 다르기 때문에 화면을 별도의 앱으로 분리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1일차에 구현한 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫날에는 총 20개의 PR을 작성하고 develop 브랜치에 병합했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공고 데이터 구조 개선&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;listings 테이블 구조 개편&lt;/li&gt;
&lt;li&gt;서울 25개 자치구 선택 기능 적용&lt;/li&gt;
&lt;li&gt;공고 원문 링크인 source_url 연결&lt;/li&gt;
&lt;li&gt;상세주소를 저장하는 detail_address 컬럼 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다중 이미지 관리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Supabase Storage 이미지 업로드&lt;/li&gt;
&lt;li&gt;공고 삭제 시 Storage 파일 함께 정리&lt;/li&gt;
&lt;li&gt;수정 화면에서 이미지 교체 및 개별 삭제&lt;/li&gt;
&lt;li&gt;공고 하나당 사진을 최대 20장까지 등록&lt;/li&gt;
&lt;li&gt;목록 카드에는 대표 이미지 한 장만 표시&lt;/li&gt;
&lt;li&gt;상세 화면에는 등록된 전체 사진 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공고 조회 화면&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신청 마감일까지 남은 날짜를 디데이로 표시&lt;/li&gt;
&lt;li&gt;마감 임박순 정렬&lt;/li&gt;
&lt;li&gt;마감된 공고를 목록 뒤로 이동&lt;/li&gt;
&lt;li&gt;마감 공고의 글자를 흐린 회색으로 표시&lt;/li&gt;
&lt;li&gt;페이지네이션 적용&lt;/li&gt;
&lt;li&gt;제곱미터와 평수 함께 표시&lt;/li&gt;
&lt;li&gt;면적&amp;middot;모집인원&amp;middot;보증금&amp;middot;월세 기준 정렬&lt;/li&gt;
&lt;li&gt;카드 가독성 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면적은 다음과 같이 표시했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;전용 13.98㎡ (약 4.2평)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관리자 화면&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공고 등록 후 기존 입력값 유지&lt;/li&gt;
&lt;li&gt;입력 초기화 버튼 추가&lt;/li&gt;
&lt;li&gt;금액 입력 시 쉼표와 만&amp;middot;억 단위 표시&lt;/li&gt;
&lt;li&gt;수정 화면의 뒤로가기 동작 개선&lt;/li&gt;
&lt;li&gt;새로고침 후에도 관리자 로그인 세션 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 목록만 보면 순조롭게 진행된 것 같지만, 실제로는 데이터가 삭제되거나 화면 상태가 예상대로 작동하지 않는 문제를 여러 번 만났다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 1. 사진 한 장을 지웠는데 열 장이 모두 사라졌다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 위험했던 문제는 다중 이미지 삭제 기능에서 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진 10장이 등록된 공고에서 한 장만 삭제했는데, 나머지 사진과 Supabase Storage 파일까지 모두 사라졌다. Storage 파일은 한 번 삭제하면 되돌리기 어렵기 때문에 특히 큰 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 로그에는 다음과 같이 정상 처리된 것으로 기록됐다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;PUT /admin/listings/245/images HTTP/1.1 200 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 아니라 200 OK가 반환됐다는 점이 중요했다. 서버는 잘못 작동한 것이 아니라, 프론트엔드에서 받은 요청을 그대로 처리했기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드는 남길 사진 목록을 다음과 같은 튜플 목록으로 전송하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;data = [
    (&quot;kept_image_urls&quot;, url)
    for url in kept_image_urls
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 httpx에서 data에 튜플 목록을 직접 넣으면 의도한 폼 데이터로 처리되지 않았다. 그 결과 kept_image_urls가 서버에 전달되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 기본값인 빈 목록을 받고 다음과 같이 판단했다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;남길 사진이 0장이다.
&amp;rarr; 기존 사진을 모두 삭제한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트가 문제를 발견하지 못한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 백엔드 테스트에서는 올바른 사전 형태의 데이터를 직접 넣고 있었다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;client.put(
    &quot;/admin/listings/1/images&quot;,
    data={
        &quot;kept_image_urls&quot;: [PHOTO_A, PHOTO_C]
    },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드만 확인하는 테스트였기 때문에 프론트엔드가 실제로 어떤 형식의 요청을 만드는지는 검사하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버 코드는 정상이었지만 서버에 요청을 보내는 화면 코드가 잘못되어 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 요청 데이터를 올바른 사전 형식으로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;data = {
    &quot;kept_image_urls&quot;: list(kept_image_urls),
    &quot;keep_count&quot;: len(kept_image_urls),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 keep_count도 함께 전송하도록 했다. 사용자가 몇 장을 남기려고 했는지 서버가 확인할 수 있게 만든 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;if keep_count != len(kept_image_urls):
    raise HTTPException(
        status_code=400,
        detail=&quot;남길 사진 목록이 온전하지 않습니다. 아무것도 지우지 않았습니다.&quot;,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;keep_count는 기본값 없이 필수 값으로 지정했다. 요청 본문이 사라지면 삭제 작업을 실행하지 않고 422 오류로 중단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 요청수정 전수정 후&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;요청 본문 없음&lt;/td&gt;
&lt;td&gt;모든 사진 삭제&lt;/td&gt;
&lt;td&gt;422 오류, 삭제하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3장을 남긴다고 했지만 목록이 0장&lt;/td&gt;
&lt;td&gt;모든 사진 삭제&lt;/td&gt;
&lt;td&gt;400 오류, 삭제하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드가 실제 요청 본문을 정상적으로 만드는지 확인하는 테스트도 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 200 OK는 서버가 요청을 처리했다는 뜻이지, 사용자가 원한 결과가 나왔다는 뜻은 아니라는 것을 배웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 2. 브라우저 뒤로가기가 작동하지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 수정 화면에서 브라우저 뒤로가기를 누르면 주소는 변경되지만 화면은 그대로 남아 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 두 가지였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;같은 주소도 방문 기록에 계속 추가되는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면 상태를 st.query_params에 저장하고 있었는데, 화면을 다시 그릴 때마다 같은 값을 반복해서 기록했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;st.query_params.from_dict(params)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 같아도 브라우저 방문 기록이 계속 추가되기 때문에 뒤로가기를 눌러도 동일한 화면으로 돌아오고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 값이 변경됐을 때만 기록하도록 수정했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;if st.query_params.to_dict() != params:
    st.query_params.from_dict(params)
    st.rerun()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 수정하자 같은 화면의 방문 기록이 여러 번 쌓이는 문제가 해결됐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Streamlit이 뒤로가기 이벤트에 반응하지 않는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방문 기록을 정리해도 주소만 변경되고 Streamlit 화면은 다시 실행되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저의 뒤로가기 이벤트인 popstate가 발생하면 페이지를 새로고침하도록 JavaScript 코드를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;window.addEventListener(&quot;popstate&quot;, function () {
    window.location.reload();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소와 화면 상태를 함께 사용하려면 브라우저의 동작과 Streamlit의 화면 실행 방식까지 모두 고려해야 한다는 것을 알게 됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 3. 입력 초기화 버튼이 값을 지우지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공고 등록 후 비슷한 공고를 연속으로 입력할 수 있도록 기존 입력값을 유지했다. 별도로 입력 초기화 버튼도 만들었지만, 버튼을 눌러도 화면의 값은 그대로 남아 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;st.session_state에서 키를 삭제하는 것만으로는 해결되지 않았다. st.form 내부의 입력값 일부가 브라우저 쪽에 남아 있었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력창 키에 번호를 붙여 초기화할 때마다 새로운 입력창을 만드는 방식으로 해결했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def field_key(name: str, nonce: int) -&amp;gt; str:
    return f&quot;create-{name}-{nonce}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화 버튼을 누르면 nonce 값을 1씩 증가시킨다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;create-title-1
&amp;rarr; 초기화
create-title-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit은 키가 달라진 입력창을 새로운 위젯으로 판단하기 때문에 빈 입력칸을 다시 만들 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 4. 전체 정렬을 화면에서 하면 안 되는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 화면에 면적&amp;middot;모집인원&amp;middot;보증금&amp;middot;월세 기준의 정렬 기능을 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 프론트엔드에서 받아 온 공고 목록을 다시 정렬하려고 했다. 하지만 화면은 전체 공고가 아니라 한 페이지에 표시할 10건만 받아 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 화면에서 정렬하면 전체 112건을 정렬하는 것이 아니라 현재 페이지의 10건만 정렬하게 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;잘못된 처리 순서
페이지 나누기 &amp;rarr; 현재 페이지 10건 정렬

올바른 처리 순서
전체 데이터 정렬 &amp;rarr; 페이지 나누기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 기준으로 정렬하기 위해 정렬 기능을 백엔드로 옮겼다. 프론트엔드는 사용자가 선택한 정렬 기준만 서버에 전달한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;SORT_COLUMNS = {
    &quot;area_desc&quot;: (&quot;area_sqm&quot;, True),
    &quot;area_asc&quot;: (&quot;area_sqm&quot;, False),
    &quot;deposit_desc&quot;: (&quot;deposit&quot;, True),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 값을 가진 공고의 순서가 계속 바뀌지 않도록 id를 보조 정렬 기준으로 추가했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;return (
    query
    .order(column, desc=desc, nullsfirst=False)
    .order(&quot;id&quot;, desc=True)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nullsfirst=False: 값이 없는 공고는 뒤로 이동&lt;/li&gt;
&lt;li&gt;id desc: 값이 같다면 최근에 등록한 공고를 먼저 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 공고 112건으로 확인한 결과 페이지가 바뀌어도 정렬 순서가 자연스럽게 이어졌다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1페이지: 59.53㎡ &amp;rarr; 56.96㎡
2페이지: 56.55㎡ &amp;rarr; 52.29㎡
3페이지: 51.36㎡ &amp;rarr; 47.02㎡
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보조 정렬 기준이 없으면 같은 값을 가진 데이터의 순서가 요청할 때마다 달라져 페이지 사이에 중복이나 누락이 생길 수 있다는 점도 배웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 5. 상세설명의 줄바꿈이 사라졌다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세설명을 여러 줄로 저장했지만 화면에서는 한 줄로 이어져 표시됐다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;신청자격 : 서울 거주 1인 무주택세대구성원
소득기준 : 도시근로자 월평균 소득 70% 이하
자산기준 : 총자산 3억 4,500만 원 이하
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마크다운에서는 일반적인 줄바꿈 하나를 공백처럼 처리할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세설명을 한 줄씩 나누어 표시하고, 항목 이름은 자동으로 굵게 변환했다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;def format_description_line(line: object) -&amp;gt; str:
    text = (&quot;&quot; if line is None else str(line)).strip()

    if not text:
        return &quot;&quot;

    for separator in (&quot; : &quot;, &quot;: &quot;, &quot; :&quot;, &quot;:&quot;):
        if separator in text:
            label, _, value = text.partition(separator)
            label = label.strip()
            value = value.strip()

            if label and value:
                return f&quot;**{label}** : {value}&quot;

    return text
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 상세설명이 다음과 같이 보기 쉽게 표시됐다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;신청자격 : 서울 거주 1인 무주택세대구성원
소득기준 : 도시근로자 월평균 소득 70% 이하
자산기준 : 총자산 3억 4,500만 원 이하
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사진 여러 장을 별도 테이블로 관리한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 listings.image_url에는 이미지 주소 하나만 저장할 수 있었다. 하지만 실제 청약 공고에는 약도, 건물 외관, 방, 부엌, 화장실 등 10장 이상의 사진이 포함돼 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 저장 방식을 비교했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분별도 테이블배열 컬럼&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;저장 방식&lt;/td&gt;
&lt;td&gt;사진 한 장을 한 행으로 저장&lt;/td&gt;
&lt;td&gt;한 컬럼에 여러 URL 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사진 한 장 삭제&lt;/td&gt;
&lt;td&gt;해당 행만 삭제&lt;/td&gt;
&lt;td&gt;배열 전체를 다시 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사진 순서 변경&lt;/td&gt;
&lt;td&gt;sort_order 수정&lt;/td&gt;
&lt;td&gt;배열 전체를 다시 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기존 테이블 변경&lt;/td&gt;
&lt;td&gt;최소화 가능&lt;/td&gt;
&lt;td&gt;listings 스키마 변경 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 사진 전용 listing_images 테이블을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE IF NOT EXISTS listing_images (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    listing_id BIGINT NOT NULL
        REFERENCES listings(id) ON DELETE CASCADE,
    image_url TEXT NOT NULL,
    sort_order INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT uq_listing_images_order
        UNIQUE (listing_id, sort_order)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 listings.image_url은 삭제하지 않고 목록 카드에서 사용하는 대표 이미지로 유지했다. 덕분에 기존 화면과 테스트를 크게 변경하지 않고 다중 이미지 기능을 추가할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 팀원이 같은 시기에 listings 테이블에 좌표 컬럼을 추가하고 있었기 때문에, 별도 테이블을 사용함으로써 Git 충돌도 피할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB와 Storage는 별도로 관리해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ON DELETE CASCADE를 설정하면 공고가 삭제될 때 listing_images의 DB 행도 함께 삭제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Supabase Storage에 저장된 실제 이미지 파일은 자동으로 삭제되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 공고를 삭제하기 전에 이미지 URL 목록을 먼저 가져와야 한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;image_urls = list(current_listing.images)

deleted_listing = listing_delete(listing_id)

for image_url in image_urls:
    delete_listing_image(image_url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 행을 먼저 삭제하면 어떤 Storage 파일을 삭제해야 하는지 알 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Storage 파일을 삭제할 때는 다른 공고가 같은 이미지를 사용하고 있지 않은지도 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 참조는 두 곳에 존재한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;listings.image_url
└── 목록 카드의 대표 이미지

listing_images.image_url
└── 공고에 등록된 전체 이미지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지가 사용 중인지 확인하다가 오류가 발생하면 삭제하지 않는 방향으로 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용되지 않는 파일이 잠시 남는 것보다, 사용 중인 파일을 잘못 지워 복구하지 못하는 상황이 더 위험하기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1일차 개발 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목결과&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;병합한 PR&lt;/td&gt;
&lt;td&gt;20개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백엔드 테스트&lt;/td&gt;
&lt;td&gt;140개 통과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관리자 화면 테스트&lt;/td&gt;
&lt;td&gt;154개 통과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자 화면 테스트&lt;/td&gt;
&lt;td&gt;154개 통과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;등록된 실제 공고&lt;/td&gt;
&lt;td&gt;112건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공고별 최대 이미지&lt;/td&gt;
&lt;td&gt;20장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 기능을 구현했지만, 가장 크게 배운 부분은 기능의 개수보다 데이터를 안전하게 다루는 방법이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트를 통해 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 200 OK가 반드시 사용자가 원한 결과를 의미하는 것은 아니다. 서버가 받은 요청을 정상적으로 처리했다는 뜻일 뿐이므로, 프론트엔드가 어떤 요청을 보냈는지도 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 테스트가 모두 통과해도 안전하다고 단정할 수 없다. 서버만 테스트하고 화면에서 만들어지는 실제 요청을 검사하지 않으면 중요한 오류가 남을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, 이미지 삭제처럼 되돌리기 어려운 작업은 요청 데이터가 온전한지 먼저 검증해야 한다. 조금이라도 이상하면 삭제를 중단하는 편이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷째, 데이터베이스와 파일 저장소는 별도로 관리된다. DB 행을 삭제해도 Storage 파일은 남을 수 있기 때문에 두 곳을 모두 정리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 팀 프로젝트에서는 코드를 작성하기 전에 팀원이 어떤 파일과 테이블을 수정하고 있는지 먼저 확인해야 한다. 설계를 조금 바꾸는 것만으로도 불필요한 Git 충돌을 피할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 작업 계획&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 아래 기능을 구현할 예정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gemini API를 이용한 청약 공고 자동 입력&lt;/li&gt;
&lt;li&gt;공고 URL을 입력하면 주요 항목을 자동으로 추출하는 기능&lt;/li&gt;
&lt;li&gt;관리자 화면의 디데이 표시와 마감순 정렬&lt;/li&gt;
&lt;li&gt;등록된 사진의 순서를 변경하는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫날부터 예상하지 못한 오류를 많이 만났지만, 단순히 기능을 만드는 것을 넘어 실제 서비스에서 데이터를 안전하게 관리하는 방법을 경험할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#FastAPI #Streamlit #Supabase #PostgreSQL #공공임대청약 #미니프로젝트 #다중이미지업로드 #페이지네이션 #GitHub협업 #AI오케스트레이션&lt;/p&gt;</description>
      <category>AI 오케스트레이션 캠프/미니프로젝트</category>
      <category>ai오케스트레이션</category>
      <category>FastAPI</category>
      <category>GitHub협업</category>
      <category>PostgreSQL</category>
      <category>streamlit</category>
      <category>supabase</category>
      <category>공공임대청약</category>
      <category>다중이미지업로드</category>
      <category>미니프로젝트</category>
      <category>페이지네이션</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/249</guid>
      <comments>https://laycoder.tistory.com/249#entry249comment</comments>
      <pubDate>Mon, 10 Aug 2026 18:14:02 +0900</pubDate>
    </item>
    <item>
      <title>22일차 회고｜다중 이미지 업로드&amp;middot;청약 공고 등록｜AI 오케스트레이션 개발자 국비지원</title>
      <link>https://laycoder.tistory.com/248</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 새로운 수업 진도 대신 1차 미니프로젝트의 청약 공고 관리 기능을 보완하고 직접 테스트했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업의 핵심은 하나의 청약 공고에 여러 장의 이미지를 등록하고, 공고문에서 추출한 정보를 실제 서비스 형식에 맞게 입력하는 것이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다중 이미지 업로드 기능 보완&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 청약 공고마다 대표 이미지 한 장만 사용할 수 있었다. 하지만 실제 공고문에는 건물 전경뿐만 아니라 약도, 주차장, 방 내부, 부엌, 화장실 등 여러 사진이 포함될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 반영하여 하나의 공고에 최대 20장의 사진을 등록할 수 있도록 기능을 보완했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대 업로드 장수: 20장&lt;/li&gt;
&lt;li&gt;이미지 한 장의 최대 크기: 5MB&lt;/li&gt;
&lt;li&gt;한 번에 올릴 수 있는 전체 용량: 60MB&lt;/li&gt;
&lt;li&gt;이미지 업로드 제한 시간: 15초에서 180초로 변경&lt;/li&gt;
&lt;li&gt;선택한 이미지의 개수와 전체 용량 표시&lt;/li&gt;
&lt;li&gt;업로드 전 이미지 미리보기 제공&lt;/li&gt;
&lt;li&gt;첫 번째 사진을 목록의 대표 이미지로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20장에 각각 5MB를 적용하면 최대 100MB가 될 수 있다. 이렇게 큰 파일을 한 번에 보내면 업로드가 중간에 멈출 가능성이 있어 전체 용량을 60MB로 제한했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 장수만 늘리는 것이 아니라 실제 업로드 과정에서 발생할 수 있는 문제까지 고려해야 한다는 점을 알게 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자&amp;middot;관리자 상세 화면 개선&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공고 목록에서는 기존처럼 대표 이미지 한 장만 보여주고, 상세 정보를 펼치면 등록된 전체 사진을 확인할 수 있도록 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세 화면에서는 사진이 한 줄에 네 장씩 배치된다. 공고 목록에 모든 사진을 표시하면 화면이 복잡해질 수 있기 때문에 목록과 상세 화면의 역할을 나눈 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목록 화면: 대표 이미지 한 장 표시&lt;/li&gt;
&lt;li&gt;상세 화면: 등록된 전체 이미지 표시&lt;/li&gt;
&lt;li&gt;사용자와 관리자 화면에 같은 이미지 구성 적용&lt;/li&gt;
&lt;li&gt;공고 설명보다 위쪽에 이미지 배치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업을 통해 많은 정보를 한 화면에 무조건 보여주는 것보다 사용자가 필요한 순간에 확인할 수 있도록 구성하는 것이 중요하다는 점을 배웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정 화면의 사진 관리 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 화면에서는 공고 정보와 사진을 따로 관리할 수 있도록 기능을 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 사진마다 삭제 버튼을 제공하고, 새로운 사진을 추가할 때는 남은 등록 가능 장수를 안내하도록 구성했다. 사진을 삭제할 때는 실수를 방지하기 위해 확인 과정도 거치게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 중요하게 확인한 부분은 공고명이나 보증금 같은 일반 정보만 수정했을 때 기존 사진이 삭제되지 않는지였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 사진을 한 장씩 삭제&lt;/li&gt;
&lt;li&gt;새로운 사진만 추가&lt;/li&gt;
&lt;li&gt;사진 삭제 전 확인&lt;/li&gt;
&lt;li&gt;공고 정보 수정 시 기존 사진 유지&lt;/li&gt;
&lt;li&gt;사진을 수정해도 입력 중인 공고 정보 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능을 분리하니 수정 화면의 역할이 더 명확해지고, 사용자가 실수로 사진을 잃을 가능성도 줄일 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지 저장 과정의 예외 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 장의 이미지를 다루면 일부 사진만 업로드된 뒤 오류가 발생할 수도 있다. 이때 이미 올라간 파일을 그대로 두면 Storage에 사용하지 않는 파일이 계속 쌓인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 막기 위해 업로드 도중 실패하면 먼저 올라간 파일을 다시 삭제하도록 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 다음과 같은 상황도 함께 점검했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 공고에서 사용 중인 이미지는 삭제하지 않기&lt;/li&gt;
&lt;li&gt;공고 삭제 시 연결된 이미지도 함께 정리하기&lt;/li&gt;
&lt;li&gt;대표 이미지도 전체 이미지 목록에 포함하기&lt;/li&gt;
&lt;li&gt;정보만 수정할 때 추가 사진을 그대로 유지하기&lt;/li&gt;
&lt;li&gt;목록 데이터가 삭제되기 전에 연결된 이미지 경로 확보하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 화면에서 사진이 보이는 것만 확인하는 것이 아니라 데이터베이스와 Storage의 상태까지 함께 생각해야 한다는 것을 배웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 청약 공고 데이터 추출&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 확인을 위해 금천구 예술인주택 공고문에서 서비스 등록에 필요한 정보를 추출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 공고 안에서도 호실마다 면적과 임대조건이 달랐기 때문에 하나의 행으로 합치지 않고 총 7개 호실로 나누어 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추출한 주요 정보는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공고명과 공고번호&lt;/li&gt;
&lt;li&gt;주택명과 동&amp;middot;호수&lt;/li&gt;
&lt;li&gt;도로명주소와 지번주소&lt;/li&gt;
&lt;li&gt;전용&amp;middot;공용&amp;middot;공급면적&lt;/li&gt;
&lt;li&gt;보증금과 월 임대료&lt;/li&gt;
&lt;li&gt;신청 시작일과 마감일&lt;/li&gt;
&lt;li&gt;신청자격과 소득&amp;middot;자산 기준&lt;/li&gt;
&lt;li&gt;접수&amp;middot;계약&amp;middot;입주 일정&lt;/li&gt;
&lt;li&gt;문의처와 원문 URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공고문에 포함된 이미지도 분리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현장 약도&lt;/li&gt;
&lt;li&gt;건물 전경&lt;/li&gt;
&lt;li&gt;주차장&lt;/li&gt;
&lt;li&gt;방 내부&lt;/li&gt;
&lt;li&gt;부엌&lt;/li&gt;
&lt;li&gt;화장실&lt;/li&gt;
&lt;li&gt;연습실&lt;/li&gt;
&lt;li&gt;커뮤니티실&lt;/li&gt;
&lt;li&gt;계단&lt;/li&gt;
&lt;li&gt;옥상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 안에는 장식용 이미지도 포함되어 있었기 때문에 모든 이미지를 그대로 등록하지 않고, 실제 주택 정보와 관련된 사진만 구분해야 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상세설명 형식 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세설명은 긴 문장으로 작성하지 않고 사용자가 빠르게 내용을 확인할 수 있도록 항목형으로 정리했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;**신청자격** : 서울 거주 만 19세 이상 무주택 문화예술인
**소득기준** : 도시근로자 월평균 소득 70% 이하
**자산기준** : 소득 50% 이하인 경우 총자산 2억 4,500만 원&amp;middot;자동차 4,542만 원 이하
**공급 구성** : 총 7호(가동 3호&amp;middot;나동 4호)
**추가 자격** : 금천구 거주자 우선, 예술활동 증명서류 제출 필수
**주의사항** : 외국인은 신청할 수 없으며 관리비와 세대별 공과금은 별도입니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소, 면적, 보증금처럼 이미 별도의 입력란이 있는 정보는 상세설명에서 반복하지 않았다. 핵심 조건만 남기니 사용자가 공고의 신청 가능 여부를 더 빠르게 판단할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 기능이 새 이미지 기능 때문에 고장 나지 않았는지도 자동화 테스트로 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백엔드 테스트: 122개 통과&lt;/li&gt;
&lt;li&gt;관리자 프론트엔드 테스트: 178개 통과&lt;/li&gt;
&lt;li&gt;사용자 프론트엔드 테스트: 154개 통과&lt;/li&gt;
&lt;li&gt;기존 청약 공고 112건 조회 확인&lt;/li&gt;
&lt;li&gt;대표 이미지만 있는 기존 공고의 상세 이미지 표시 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사진 등록은 데이터베이스와 Storage에 파일을 생성하는 작업이므로, 테스트용 공고를 직접 등록하면서 최종 화면을 확인하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 사진 여러 장을 등록하는 기능이 단순히 파일 선택 기능만 추가한다고 완성되는 것이 아니라는 점을 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업로드 장수와 용량 제한, 대표 이미지 결정, 일부 업로드 실패 처리, 개별 삭제, 공고 수정 시 이미지 유지까지 여러 상황을 함께 고려해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 실제 공고문을 서비스 데이터로 옮기는 과정에서는 문서의 내용을 그대로 복사하기보다 데이터베이스 구조에 맞게 분리하고 정리하는 작업이 중요했다. 특히 호실마다 임대조건이 다른 경우에는 각각 별도의 데이터로 등록해야 검색 결과도 정확해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 테스트 공고를 직접 등록하여 관리자와 사용자 화면에서 전체 사진이 제대로 표시되는지 확인하고, 사진을 삭제하거나 공고명만 수정해도 나머지 이미지가 유지되는지 점검할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#AI오케스트레이션 #국비지원개발자과정 #미니프로젝트 #다중이미지업로드 #Streamlit #FastAPI #Supabase #청약정보서비스 #데이터전처리 #개발회고&lt;/p&gt;</description>
      <category>AI 오케스트레이션 캠프/일차별 회고</category>
      <category>ai오케스트레이션</category>
      <category>FastAPI</category>
      <category>streamlit</category>
      <category>supabase</category>
      <category>개발회고</category>
      <category>국비지원개발자과정</category>
      <category>다중이미지업로드</category>
      <category>데이터전처리</category>
      <category>미니프로젝트</category>
      <category>청약정보서비스</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/248</guid>
      <comments>https://laycoder.tistory.com/248#entry248comment</comments>
      <pubDate>Mon, 10 Aug 2026 18:11:19 +0900</pubDate>
    </item>
    <item>
      <title>SK네트웍스 Family 엔코아 AI 오케스트레이션 프리코스 5｜파이썬 내장 함수&amp;middot;클래스&amp;middot;상속&amp;middot;모듈</title>
      <link>https://laycoder.tistory.com/247</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프리코스에서는 파이썬의 내장 함수를 복습하고, 객체와 클래스부터 상속, 모듈까지 학습했다. 마지막에는 random과 datetime 모듈을 활용해 임의의 값과 날짜&amp;middot;시간을 다루는 방법도 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 객체와 클래스가 낯설게 느껴졌지만, 자동차 공장과 설계도에 비유해 생각하니 각각의 역할을 조금 더 쉽게 구분할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 파이썬 내장 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장 함수(Built-in Function)는 파이썬에 기본적으로 포함되어 있어 별도의 import 없이 바로 사용할 수 있는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 사용했던 print(), input(), len(), int()도 모두 내장 함수였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 내장 함수는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;int(), str(), list(), tuple(), set(), bool(): 자료형 변환&lt;/li&gt;
&lt;li&gt;max(), min(): 최댓값과 최솟값 반환&lt;/li&gt;
&lt;li&gt;abs(): 절댓값 반환&lt;/li&gt;
&lt;li&gt;sum(): 값의 합계 계산&lt;/li&gt;
&lt;li&gt;len(): 문자열이나 자료의 길이 확인&lt;/li&gt;
&lt;li&gt;type(): 데이터의 자료형 확인&lt;/li&gt;
&lt;li&gt;range(): 일정한 범위의 숫자 생성&lt;/li&gt;
&lt;li&gt;round(): 숫자 반올림&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;numbers = [10, 20, 30]

print(max(numbers))
print(min(numbers))
print(sum(numbers))
print(len(numbers))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;print()의 sep과 end도 다시 확인했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;print(&quot;Python&quot;, &quot;FastAPI&quot;, &quot;Streamlit&quot;, sep=&quot; | &quot;)
print(&quot;Hello&quot;, end=&quot; &quot;)
print(&quot;Python&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sep은 여러 값을 구분하는 문자를 정하고, end는 출력이 끝난 뒤 들어갈 문자를 정한다. 기본적으로 sep에는 공백, end에는 줄바꿈이 설정되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 함수가 기억나지 않을 때는 파이썬 공식 문서나 help()를 이용해 사용법을 찾아보는 습관도 중요하다는 것을 배웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 객체와 클래스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 지향 프로그래밍은 현실의 사물을 바라보는 것처럼 데이터와 기능을 하나의 단위로 묶어 관리하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스와 객체의 관계는 설계도와 실제 제품으로 생각할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스: 객체를 만들기 위한 설계도&lt;/li&gt;
&lt;li&gt;객체: 클래스를 이용해 실제로 만든 데이터&lt;/li&gt;
&lt;li&gt;속성: 객체가 가지고 있는 데이터&lt;/li&gt;
&lt;li&gt;메서드: 객체가 수행할 수 있는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동차를 예로 들면 자동차 설계도가 클래스이고, 그 설계도로 만든 각각의 자동차가 객체다. 자동차의 색상과 바퀴 수는 속성이며, 출발하거나 멈추는 동작은 메서드가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 구조의 자동차 100대를 각각 변수와 함수로 관리하면 코드를 계속 반복해야 한다. 반면 클래스를 사용하면 공통 구조를 한 곳에 정의한 뒤 필요한 만큼 객체를 만들 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 클래스 선언과 인스턴스 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬에서는 class 키워드를 사용해 클래스를 정의한다. 클래스 이름은 보통 각 단어의 첫 글자를 대문자로 작성하는 파스칼 케이스를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class Person:
    def __init__(self):
        self.hello = &quot;안녕하세요&quot;

    def greeting(self):
        print(self.hello)


james = Person()
james.greeting()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 Person은 클래스이고, james는 Person 클래스로 만든 객체다. 특정 클래스로 만든 객체라는 점을 강조할 때는 인스턴스라고 부른다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;self의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;self는 생성된 인스턴스 자기 자신을 가리킨다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;self.hello = &quot;안녕하세요&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 현재 객체에 hello라는 속성을 만들고, 그 안에 &quot;안녕하세요&quot;라는 값을 저장한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드를 호출할 때 self에 값을 직접 전달하지 않아도 파이썬이 현재 인스턴스를 자동으로 전달한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;__init__() 생성자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;__init__()은 객체가 만들어질 때 처음 한 번 실행되는 특별한 메서드다. 객체가 처음 가져야 할 속성을 설정할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class Person:
    def __init__(self, name):
        self.name = name
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Person(&quot;James&quot;)처럼 객체를 만들면 전달한 &quot;James&quot;가 name으로 들어가고, self.name에 저장된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 클래스 상속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속은 기존 클래스의 속성과 메서드를 새로운 클래스에서 이어받아 사용하는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 기능은 부모 클래스에 작성하고, 서로 다른 기능만 자식 클래스에 추가하면 코드의 중복을 줄일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health

    def attack(self):
        print(f&quot;{self.name}이 공격합니다.&quot;)


class Warrior(Character):
    def __init__(self, name, health, strength):
        super().__init__(name, health)
        self.strength = strength

    def attack(self):
        super().attack()
        print(f&quot;{self.name}이 강력한 공격을 합니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Warrior(Character)는 Warrior가 Character 클래스를 상속한다는 뜻이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;super()와 오버라이딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;super()는 부모 클래스의 생성자나 메서드를 호출할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;super().__init__(name, health)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 이용하면 부모 클래스에 이미 작성된 이름과 체력 초기화 기능을 다시 작성하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오버라이딩은 부모 클래스에서 물려받은 메서드를 자식 클래스의 목적에 맞게 다시 정의하는 것이다. 같은 attack() 메서드라도 전사는 강력한 공격을 하고, 마법사는 마법 공격을 하도록 다르게 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속을 사용하면 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통 코드의 중복을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;기존 클래스를 재사용하고 확장할 수 있다.&lt;/li&gt;
&lt;li&gt;수정해야 할 코드가 줄어 유지보수가 쉬워진다.&lt;/li&gt;
&lt;li&gt;클래스 사이의 관계를 구조적으로 표현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 파이썬 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈은 함수, 클래스, 변수 등의 파이썬 코드를 담고 있는 .py 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용하는 기능을 모듈로 분리하면 다른 파일에서 다시 사용할 수 있다. 코드가 기능별로 정리되기 때문에 프로젝트 구조를 이해하거나 수정하기도 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈을 불러올 때는 import를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import character

player = character.Character(&quot;전사&quot;, 100)
player.attack()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별칭을 지정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;import numpy as np
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈의 특정 기능만 가져오는 방법도 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;from math import sqrt

print(sqrt(9))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈의 모든 항목을 가져오는 from 모듈 import * 방식도 있지만, 서로 같은 이름을 가진 함수나 변수끼리 충돌할 수 있어 주의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 모듈 파일 이름은 변수 이름과 같은 규칙을 따라야 한다. 따라서 파일 이름을 숫자로 시작하면 정상적으로 import하기 어렵다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Jupyter Notebook 코드를 모듈로 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jupyter Notebook 파일은 기본 확장자가 .ipynb이므로 일반적인 파이썬 모듈로 사용하려면 .py 파일로 변환해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;jupyter nbconvert --to script 파일명.ipynb
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노트북 셀의 내용을 바로 파이썬 파일로 저장하는 방법도 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;%%writefile character.py

class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈로 저장하기 전에는 코드에 오류가 없는지 먼저 확인해야 한다. 불러오는 과정에서 모듈 내부 코드가 실행되기 때문에 오류가 있으면 import도 정상적으로 처리되지 않을 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. random 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;random은 임의의 숫자를 만들거나 여러 값 중 하나를 무작위로 선택할 때 사용하는 파이썬 표준 라이브러리다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import random
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 기능은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;random.random()          # 0 이상 1 미만의 실수
random.randint(1, 10)    # 1부터 10 사이의 정수
random.choice([&quot;사과&quot;, &quot;포도&quot;, &quot;오렌지&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트의 순서를 무작위로 섞을 때는 shuffle()을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;cards = [&quot;A&quot;, &quot;J&quot;, &quot;Q&quot;, &quot;K&quot;]
random.shuffle(cards)

print(cards)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 없이 여러 개의 값을 선택할 때는 sample()을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;numbers = list(range(1, 11))
selected = random.sample(numbers, 3)

print(selected)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;choice()는 하나의 값을 선택하고, sample()은 원하는 개수만큼 중복 없이 선택한다는 차이가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. datetime 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;datetime은 날짜와 시간을 만들고 계산하는 데 사용하는 표준 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 날짜와 시간은 다음과 같이 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import datetime

now = datetime.datetime.now()
print(now)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 날짜와 시간을 직접 만들 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;specific_date = datetime.datetime(2023, 5, 23, 10, 30)
print(specific_date)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;strftime()을 사용하면 날짜와 시간을 원하는 형태의 문자열로 바꿀 수 있다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;now = datetime.datetime.now()
formatted_date = now.strftime(&quot;%Y-%m-%d&quot;)

print(formatted_date)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timedelta()는 날짜와 시간의 차이를 계산할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
print(yesterday)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간대가 다른 지역의 시간으로 변환하는 방법도 살펴봤다. 날짜와 시간 기능은 기록 생성 시간, 예약 기능, 게시 날짜 처리 등 실제 프로그램의 여러 곳에서 활용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새롭게 이해한 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 학습을 통해 지금까지 자연스럽게 사용했던 print(), len(), type() 등이 모두 내장 함수라는 것을 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체와 클래스는 처음에는 추상적으로 느껴졌지만, 클래스는 공통 구조를 정의하는 설계도이고 객체는 그 설계도로 만든 실제 데이터라고 생각하니 관계가 조금 더 명확해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 상속은 단순히 부모 클래스의 기능을 가져오는 것만이 아니라, 중복되는 코드를 줄이고 프로그램을 체계적으로 관리하기 위한 방법이라는 점이 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 역시 파일을 나누는 것에서 끝나는 것이 아니라, 작성한 코드를 다른 곳에서 재사용하고 유지보수하기 쉽게 만드는 구조화 방법이라는 것을 이해했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앞으로 복습할 내용&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;self가 현재 인스턴스를 가리키는 과정&lt;/li&gt;
&lt;li&gt;__init__()을 이용한 속성 초기화&lt;/li&gt;
&lt;li&gt;부모 클래스와 자식 클래스의 관계&lt;/li&gt;
&lt;li&gt;super()와 메서드 오버라이딩&lt;/li&gt;
&lt;li&gt;직접 만든 모듈을 다른 파일에서 불러오는 방법&lt;/li&gt;
&lt;li&gt;random 함수별 차이&lt;/li&gt;
&lt;li&gt;datetime의 날짜 형식 지정자와 시간대 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프리코스에서는 파이썬에 기본으로 준비된 내장 함수부터 객체 지향 프로그래밍의 기초인 클래스와 객체를 학습했다. 이어서 생성자, 속성, 메서드, self, 상속, 오버라이딩, super()의 역할을 코드로 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 파이썬 코드를 모듈로 분리하고 import로 다시 사용하는 방법을 배웠다. 마지막으로 random과 datetime 모듈을 활용해 무작위 값과 날짜&amp;middot;시간을 처리하는 기본 방법까지 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 개념을 아는 것에 그치지 않고 작은 클래스를 직접 만들고, 파일을 모듈로 나누어 불러오는 연습을 반복해야겠다. 필요한 기능을 공식 문서에서 직접 찾아보는 습관도 함께 길러야겠다.&lt;/p&gt;</description>
      <category>AI 오케스트레이션 프리코스</category>
      <category>Datetime모듈</category>
      <category>Python</category>
      <category>random모듈</category>
      <category>객체지향프로그래밍</category>
      <category>내장함수</category>
      <category>클래스상속</category>
      <category>파이썬</category>
      <category>파이썬기초</category>
      <category>파이썬모듈</category>
      <category>파이썬클래스</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/247</guid>
      <comments>https://laycoder.tistory.com/247#entry247comment</comments>
      <pubDate>Fri, 7 Aug 2026 21:40:56 +0900</pubDate>
    </item>
    <item>
      <title>[SK네트웍스 Family 엔코아AI캠퍼스] AI 오케스트레이션 캠프 1기_8월 1주차 회고</title>
      <link>https://laycoder.tistory.com/246</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 주 학습 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8월 1주차에는 17일차부터 21일차까지 수업을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주는 회원 인증과 데이터베이스 설계부터 시작해 이미지가 포함된 상품 CRUD를 구현하고, 이를 팀 미니프로젝트에 적용하는 흐름으로 이어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이전까지 각각 연습했던 Streamlit, FastAPI, Supabase를 하나의 서비스로 연결해 보면서 프론트엔드&amp;middot;백엔드&amp;middot;데이터베이스가 어떤 방식으로 데이터를 주고받는지 조금 더 구체적으로 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주 후반에는 SH 청약 공고를 편리하게 검색할 수 있는 서비스를 만들며 관리자용 공고 관리 기능과 사용자용 공고 조회 기능을 개발했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 학습 내용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 회원 인증과 Supabase 데이터베이스 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supabase 인증 기능을 이용해 이메일 기반 회원가입과 로그인 흐름을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 사용자가 누구인지 확인하는 기능이다. 로그인한 사용자 정보를 기준으로 즐겨찾기나 개인화된 데이터를 구분하려면 인증 정보와 서비스의 데이터베이스가 자연스럽게 연결되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 ERD를 작성하며 다음과 같은 데이터의 관계를 정리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 정보&lt;/li&gt;
&lt;li&gt;관리자 정보&lt;/li&gt;
&lt;li&gt;청약 공고 정보&lt;/li&gt;
&lt;li&gt;사용자가 저장한 즐겨찾기 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블을 단순히 여러 개 만드는 것이 아니라, 각각의 데이터가 어떤 ID를 기준으로 연결되는지 먼저 설계하는 과정이 중요하다는 것을 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원들과 Supabase 테이블과 프로젝트 폴더 구조를 공유하면서 각자 맡은 CRUD 기능을 어떻게 합칠지도 함께 논의했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 이미지 업로드와 상품 CRUD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 데이터에 이미지 정보를 포함하고, 등록한 상품을 다시 조회하는 기능을 실습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 등록 과정에서는 텍스트나 숫자만 저장하는 것이 아니라 이미지 파일을 업로드하고, 저장된 이미지 경로를 데이터베이스의 상품 정보와 연결해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 조회 화면에서는 데이터베이스에서 상품 정보를 가져온 뒤 이미지까지 함께 화면에 출력하도록 구성했다. 이 과정에서 다음과 같은 전체 흐름을 확인할 수 있었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Streamlit 화면에서 상품 정보를 입력한다.&lt;/li&gt;
&lt;li&gt;FastAPI가 요청받은 데이터를 처리한다.&lt;/li&gt;
&lt;li&gt;Supabase에 상품 정보와 이미지 관련 정보를 저장한다.&lt;/li&gt;
&lt;li&gt;저장된 데이터를 다시 조회한다.&lt;/li&gt;
&lt;li&gt;상품 정보와 이미지를 Streamlit 화면에 출력한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 처리는 이미지 업로드나 외부 서버 요청처럼 시간이 걸릴 수 있는 작업을 효율적으로 처리하는 방법이다. 이번 실습을 통해 async와 await가 단순한 문법이 아니라 서버가 요청을 처리하는 방식과 관련된 개념이라는 점을 이해했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 팀 미니프로젝트 기획과 역할 분담&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주 후반부터는 지금까지 학습한 내용을 활용하는 첫 번째 팀 미니프로젝트를 본격적으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 SH 청약 공고를 사용자가 편리하게 찾아볼 수 있는 서비스를 만들기로 했다. 이 서비스는 사이트 내부에서 청약 신청을 받는 서비스가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 다음과 같은 조건으로 청약 공고를 검색하고, 관심 있는 공고의 원문 URL을 통해 실제 SH 신청 사이트로 이동하도록 돕는 것이 핵심이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자치구&lt;/li&gt;
&lt;li&gt;최대 보증금&lt;/li&gt;
&lt;li&gt;최대 월세&lt;/li&gt;
&lt;li&gt;여러 조건을 함께 적용하는 복합 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 목적을 명확하게 정리하면서 청약 신청이나 취소 기능은 개발 범위에서 제외했다. 기능을 많이 만드는 것보다 서비스가 해결하려는 문제를 정확하게 정하는 것이 먼저라는 점을 배울 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis와 같은 빠른 데이터 저장소를 어떤 기능에 적용할 수 있을지도 검토했지만, 이번 주에는 우선 Streamlit&amp;middot;FastAPI&amp;middot;Supabase를 연결해 핵심 기능을 완성하는 데 집중했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 청약 공고 관리 및 검색 기능 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 화면에서는 청약 공고를 등록하고 관리할 수 있도록 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 관리자 계정으로 로그인한 뒤 공고 등록 화면으로 이동해 다음 정보를 입력하고 저장하는 과정을 테스트했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공고명&lt;/li&gt;
&lt;li&gt;자치구&lt;/li&gt;
&lt;li&gt;보증금&lt;/li&gt;
&lt;li&gt;월세&lt;/li&gt;
&lt;li&gt;접수 기간&lt;/li&gt;
&lt;li&gt;공고 원문 URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자치구 선택 상자와 날짜 입력 기능도 실제 화면에서 점검했다. 입력한 데이터가 Supabase에 정상적으로 저장되고, 사용자 화면에서 조회되는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서는 기존 청약 공고 조회 API의 검색 로직을 점검하고 다음 조건을 사용할 수 있도록 수정했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지역 검색&lt;/li&gt;
&lt;li&gt;최대 보증금 검색&lt;/li&gt;
&lt;li&gt;최대 월세 검색&lt;/li&gt;
&lt;li&gt;복합 조건 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 조건만 검색하는 것에서 끝나지 않고 여러 조건을 동시에 전달했을 때도 올바른 결과가 나오는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 검색 결과에서 필요한 공고를 확인하고, 원문 URL을 통해 실제 신청 사이트로 이동할 수 있다. 이를 통해 우리 서비스가 청약 신청 사이트가 아니라 공고 탐색을 도와주는 서비스라는 방향도 더욱 분명해졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 및 문제 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주 실습에서는 각각의 기능이 따로 작동하는지만 확인하지 않고 전체 데이터 흐름을 연결하는 데 집중했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 화면이 정상적으로 보여도 API 요청이 올바르게 전달되지 않거나, 백엔드에서 사용하는 필드 이름과 데이터베이스의 컬럼 이름이 다르면 기능이 제대로 작동하지 않을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음 순서로 문제를 확인했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Streamlit에서 입력한 값 확인&lt;/li&gt;
&lt;li&gt;FastAPI로 전달되는 요청 데이터 확인&lt;/li&gt;
&lt;li&gt;백엔드 검색 조건과 처리 결과 확인&lt;/li&gt;
&lt;li&gt;Supabase에 저장된 실제 데이터 확인&lt;/li&gt;
&lt;li&gt;사용자 화면에 출력되는 결과 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 검색 기능에서는 지역, 보증금, 월세 조건을 각각 테스트한 뒤 여러 조건을 함께 적용했다. 작은 단위부터 확인하니 오류가 발생한 위치를 찾기가 쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 화면도 로그인부터 공고 등록, 데이터 저장, 사용자 화면 조회까지 실제 사용 순서대로 테스트했다. 코드를 작성하는 것뿐만 아니라 사용자의 입장에서 직접 기능을 확인하는 과정이 중요하다는 것을 느꼈다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KPT 회고&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Keep&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit, FastAPI, Supabase를 각각 분리된 기술로 보지 않고 하나의 서비스 흐름으로 연결해 본 점은 계속 유지하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능을 만든 뒤 화면에서 직접 테스트하고, 문제가 생기면 프론트엔드&amp;middot;백엔드&amp;middot;데이터베이스를 순서대로 확인한 방식도 효과적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트를 진행하면서 서비스의 목적과 개발 범위를 먼저 정리한 것도 좋았다. 덕분에 불필요한 청약 신청&amp;middot;취소 기능을 제외하고 공고 검색이라는 핵심 기능에 집중할 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 구조, API 요청값, 응답 데이터의 형태를 동시에 확인해야 해서 처음에는 어디에서 문제가 발생했는지 찾기 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 각자 만든 기능을 합칠 때는 파일 구조와 변수명, 데이터베이스 컬럼에 대한 공통 기준이 필요하다는 점도 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 기능 구현에 집중하다 보면 예외 상황이나 사용자 입장에서 불편한 부분을 뒤늦게 발견할 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Try&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 기능을 만들기 전에 요청값과 응답값을 간단하게 문서로 정리하고, 팀원들과 같은 기준을 공유해 보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 기능은 다음과 같이 테스트 조건을 미리 나누어 확인할 계획이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 조건이 없는 경우&lt;/li&gt;
&lt;li&gt;조건을 하나만 사용한 경우&lt;/li&gt;
&lt;li&gt;여러 조건을 함께 사용한 경우&lt;/li&gt;
&lt;li&gt;검색 결과가 없는 경우&lt;/li&gt;
&lt;li&gt;잘못된 값이 전달된 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 작업도 기능 단위로 브랜치와 커밋을 나누어 변경 내용을 쉽게 확인할 수 있도록 연습할 예정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 주 계획&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 주에는 이번 주에 구현한 청약 공고 관리 및 검색 기능을 다시 점검하고, 관리자 화면과 사용자 화면의 연결을 더욱 안정적으로 다듬을 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즐겨찾기처럼 로그인한 사용자와 연결되는 기능도 데이터베이스 관계를 정확하게 확인하면서 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능을 추가하는 것에만 집중하지 않고 다음 항목도 함께 확인하고 싶다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 데이터 방지&lt;/li&gt;
&lt;li&gt;잘못된 입력값 처리&lt;/li&gt;
&lt;li&gt;검색 결과가 없을 때의 안내&lt;/li&gt;
&lt;li&gt;공고 원문 URL 이동 확인&lt;/li&gt;
&lt;li&gt;팀원 코드 통합 후 전체 기능 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주에는 배운 기술을 실제 팀 서비스에 적용하기 시작했다는 점에서 의미가 컸다. 아직 기능을 연결하고 오류를 찾는 과정이 쉽지는 않지만, 하나의 서비스가 만들어지는 전체 흐름을 조금씩 이해하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#AI오케스트레이션 #국비지원개발자과정 #엔코아AI캠퍼스 #Streamlit #FastAPI #Supabase #회원인증 #CRUD #팀프로젝트 #청약정보서비스&lt;/p&gt;</description>
      <category>AI 오케스트레이션 캠프/주간 회고</category>
      <category>ai오케스트레이션</category>
      <category>crud</category>
      <category>FastAPI</category>
      <category>streamlit</category>
      <category>supabase</category>
      <category>국비지원개발자과정</category>
      <category>엔코아AI캠퍼스</category>
      <category>청약정보서비스</category>
      <category>팀프로젝트</category>
      <category>회원인증</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/246</guid>
      <comments>https://laycoder.tistory.com/246#entry246comment</comments>
      <pubDate>Fri, 7 Aug 2026 21:29:58 +0900</pubDate>
    </item>
    <item>
      <title>21일차 회고｜Redis&amp;middot;GitHub 협업과 청약 공고 검색｜AI 오케스트레이션 개발자 국비지원</title>
      <link>https://laycoder.tistory.com/245</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Redis의 역할과 연결 방법을 학습하고, GitHub 협업 환경을 정리했다. 이후 팀 미니프로젝트에서는 서울 25개 자치구를 이용한 청약 공고 검색 기능을 구현하고 관리자 화면부터 API와 DB까지 통합 테스트했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis의 역할과 연결 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 데이터를 메모리에서 빠르게 읽고 쓸 수 있는 저장소다. 모든 데이터를 영구적으로 보관하기보다는 로그인 세션, 캐시, 현재 진행 중인 채팅처럼 빠른 처리가 필요한 정보를 관리할 때 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 연결이 제대로 되지 않을 때는 다음 항목을 점검했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.env의 REDIS_URL이 올바르게 작성됐는지 확인&lt;/li&gt;
&lt;li&gt;환경 변수 이름이나 주소가 중복되지 않았는지 확인&lt;/li&gt;
&lt;li&gt;가상환경이 활성화됐는지 확인&lt;/li&gt;
&lt;li&gt;Redis 연결에 필요한 패키지가 설치됐는지 확인&lt;/li&gt;
&lt;li&gt;.env를 수정한 후 실행 중인 서버 재시작&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트에 Redis를 활용할 방법도 생각해봤다. 사용자가 챗봇과 대화하는 동안에는 Redis에 현재 대화를 임시로 저장하고, 대화가 끝나면 필요한 내용을 요약해 기존 DB에 저장하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이는 오늘 실제로 구현한 기능이 아니라 프로젝트에 적용할 수 있는 구조를 검토한 단계다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub 팀 협업 환경 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔코아 조직 계정의 팀 프로젝트 저장소와 멤버 초대 상태를 확인하고 협업 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 바로 develop 브랜치에 올리는 것이 아니라, 각자 작업한 브랜치에서 develop 브랜치로 Pull Request를 보내기로 했다. 이후 작성자를 제외한 팀원 한 명 이상이 코드를 확인하고 승인해야 병합할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 Pull Request는 단순히 코드를 합치는 기능이 아니라, 팀원이 변경 내용을 확인하고 오류를 줄이는 검토 과정이라는 점을 다시 이해했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;청약 공고 서비스의 방향 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서비스는 사이트 안에서 청약 신청을 받는 서비스가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 SH 청약 공고를 자치구, 보증금, 월세 등의 조건으로 편하게 검색하고, 관심 있는 공고의 원문 URL을 통해 실제 신청 사이트로 이동할 수 있도록 돕는 서비스다. 따라서 사이트 내부의 청약 신청과 신청 취소 기능은 개발 범위에 포함하지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자치구 기반 공고 검색 기능 개선&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 검색 기능에서는 다음 조건을 사용할 수 있도록 백엔드 로직을 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지역 검색&lt;/li&gt;
&lt;li&gt;최대 보증금 검색&lt;/li&gt;
&lt;li&gt;최대 월세 검색&lt;/li&gt;
&lt;li&gt;여러 조건을 함께 사용하는 복합 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 사용자가 지역을 직접 입력하면 같은 장소도 서울, 서울특별시 관악구, 관악구처럼 서로 다르게 저장될 수 있다는 문제를 발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 관리자와 사용자 화면에 서울 25개 자치구 선택 기능을 적용했다. 자치구 목록은 여러 화면에 반복해서 작성하지 않고 다음 상수 파일로 분리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FE_Admin/core/constants.py&lt;/li&gt;
&lt;li&gt;FE_User/core/constants.py&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 등록 화면은 처음 열었을 때 자치구를 선택해 주세요가 표시되며, 반드시 자치구를 하나 선택해야 공고를 등록할 수 있도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자와 사용자 조회 화면에는 다음 기능을 적용했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 검색값은 전체&lt;/li&gt;
&lt;li&gt;특정 자치구를 선택하면 해당 지역 공고만 조회&lt;/li&gt;
&lt;li&gt;자치구&amp;middot;최대 보증금&amp;middot;최대 월세 조건을 함께 적용&lt;/li&gt;
&lt;li&gt;공고 카드의 지역 표현을 자치구로 통일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드의 DB 컬럼과 API 파라미터는 기존 location을 그대로 사용했다. 기존 백엔드가 부분 일치 검색을 지원하고 있어 새로운 컬럼을 만들거나 DB 구조를 변경할 필요는 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 기능과 관계없는 즐겨찾기와 공고 삭제 코드는 수정하지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방과 후 통합 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방과 후에는 Claude Code의 브라우저 기능을 이용해 실제 Streamlit 화면을 테스트했다. 관리자 로그인은 직접 진행하고, 이후 테스트 공고 등록과 검색 검증은 Claude Code를 활용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위해 다음 공고 두 건을 등록했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;[TEST] 관악구 매물
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자치구: 관악구&lt;/li&gt;
&lt;li&gt;보증금: 20,000,000원&lt;/li&gt;
&lt;li&gt;월세: 300,000원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;[TEST] 성북구 매물
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자치구: 성북구&lt;/li&gt;
&lt;li&gt;보증금: 40,000,000원&lt;/li&gt;
&lt;li&gt;월세: 500,000원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 조회 화면에서는 전체&amp;middot;관악구&amp;middot;성북구&amp;middot;강남구 검색을 확인했다. 강남구를 선택했을 때 두 테스트 공고가 표시되지 않는 것도 정상적으로 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보증금과 월세의 경계값도 테스트했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관악구 + 최대 보증금 20,000,000원: 표시&lt;/li&gt;
&lt;li&gt;관악구 + 최대 보증금 19,999,999원: 미표시&lt;/li&gt;
&lt;li&gt;성북구 + 최대 월세 500,000원: 표시&lt;/li&gt;
&lt;li&gt;성북구 + 최대 월세 499,999원: 미표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 화면에서도 자치구 검색이 예상대로 작동했으며, 공고 제목&amp;middot;주택명&amp;middot;자치구&amp;middot;면적&amp;middot;모집 인원&amp;middot;보증금&amp;middot;월세&amp;middot;신청 기간&amp;middot;상세 설명&amp;middot;원문 URL이 정상적으로 표시됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 GET 요청을 이용해 DB와 검색 API까지 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관악구와 성북구 테스트 공고가 각각 1건씩 존재&lt;/li&gt;
&lt;li&gt;location에 자치구가 올바르게 저장&lt;/li&gt;
&lt;li&gt;자치구 검색 API가 정상적으로 결과 반환&lt;/li&gt;
&lt;li&gt;등록 후 입력 폼과 자치구 선택값 초기화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 테스트를 통과해 추가 코드 수정은 하지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 중 확인한 주의 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 공고 중에는 location이 서울로 저장된 데이터가 있었다. 이 데이터가 관악구나 성북구 검색 결과에 나오지 않는 것은 검색 오류가 아니라 기존 데이터와 새 데이터의 저장 형식이 다르기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면적이 화면에서 25.0㎡처럼 소수점 한 자리로 표시됐지만 실제 저장값에는 문제가 없었다. 단순한 화면 표시 문제이므로 이번 작업에서는 수정하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 로그인은 Streamlit의 session_state를 이용하고 있어 브라우저를 새로고침하면 로그인이 해제될 수 있다는 점도 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 검색 코드를 만드는 것만큼 DB에 저장되는 데이터의 형식을 통일하는 것이 중요하다는 점을 배웠다. 사용자가 값을 자유롭게 입력하게 하는 것보다 고정된 선택지를 제공하면 데이터가 일정해지고 검색 결과도 더 정확해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 구현 후에는 한 화면만 확인하는 것이 아니라 관리자 등록, 관리자 조회, 사용자 조회, API 응답, DB 저장 결과를 하나의 흐름으로 테스트해야 실제 서비스가 제대로 연결됐는지 판단할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 코딩 도구를 사용할 때도 작업을 지나치게 잘게 나누기보다 관련 코드 확인부터 구현, 실행, 오류 수정, 핵심 테스트까지 하나의 기능 단위로 요청하는 편이 효율적이라는 점을 경험했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 작업에서는 즐겨찾기와 마이페이지, 즐겨찾기가 많은 공고 순 정렬, 원문 URL 이동, 전체 통합 테스트, README와 실행 방법, 배포 환경을 차례대로 점검할 예정이다.&lt;/p&gt;</description>
      <category>AI 오케스트레이션 캠프/일차별 회고</category>
      <category>AI오케스트레이션개발자</category>
      <category>FastAPI</category>
      <category>GitHub협업</category>
      <category>pullrequest</category>
      <category>Redis</category>
      <category>streamlit</category>
      <category>국비지원개발자과정</category>
      <category>미니프로젝트</category>
      <category>청약공고검색</category>
      <category>통합테스트</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/245</guid>
      <comments>https://laycoder.tistory.com/245#entry245comment</comments>
      <pubDate>Fri, 7 Aug 2026 21:17:35 +0900</pubDate>
    </item>
    <item>
      <title>20일차 회고｜Git push 오류 해결&amp;middot;공공주택 미니프로젝트 설계｜AI 오케스트레이션 개발자 국비지원</title>
      <link>https://laycoder.tistory.com/244</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;b&gt;1차 미니프로젝트의 기존 프로토타입을 점검하고, 앞으로 구현할 기능과 협업 방식을 구체화&lt;/b&gt;했다. 작업 중 발생한 GitHub push 오류도 직접 원인을 확인하며 해결했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub push 오류 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 프로젝트를 GitHub에 올리려 했지만 다음과 같은 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;Can't push refs to remote.
Try running &quot;Pull&quot; first to integrate your changes.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 git status로 확인했을 때 작업 폴더는 깨끗한 상태였다. 하지만 push를 실행하자 origin이라는 원격 저장소를 찾을 수 없다는 메시지가 나왔다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;git remote -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 결과 원격 저장소 이름이 일반적으로 사용하는 origin이 아니라 a로 등록되어 있었다. 다음 명령어로 이름을 변경했다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;git remote rename a origin
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다시 push했지만, 이번에는 원격 저장소의 커밋이 로컬보다 앞서 있어 push가 거절됐다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Updates were rejected because the tip of your current branch is behind
its remote counterpart.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격 변경 내용을 먼저 가져오면서 커밋 기록을 깔끔하게 유지하기 위해 rebase 방식을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;git pull --rebase origin main
git push -u origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 원격 변경 내용과 로컬 작업이 정상적으로 정리됐고, GitHub push도 완료할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 오류를 해결하며 &lt;b&gt;작업 파일에 문제가 없어도 원격 저장소 이름이나 커밋 순서 때문에 push가 실패할 수 있다&lt;/b&gt;는 점을 배웠다. 오류가 발생하면 바로 여러 명령어를 입력하기보다 git status와 git remote -v부터 확인하는 것이 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 나타난 PowerShell의 가상환경 실행 정책 오류는 Git push 문제와는 별개의 문제라는 것도 구분할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공공주택 청약 서비스 프로토타입 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀에서는 현재 만들어진 &lt;b&gt;서울 공공주택 청약 정보 서비스&lt;/b&gt;를 실행해 보며 구현된 기능을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로토타입에는 다음과 같은 기능이 들어가 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Supabase 이메일 기반 회원가입과 로그인&lt;/li&gt;
&lt;li&gt;사용자용 청약 공고 조회&lt;/li&gt;
&lt;li&gt;관심 공고 즐겨찾기&lt;/li&gt;
&lt;li&gt;관리자용 공고 등록&amp;middot;조회&amp;middot;삭제&lt;/li&gt;
&lt;li&gt;즐겨찾기가 많은 공고 순위 조회&lt;/li&gt;
&lt;li&gt;사용자별 즐겨찾기 내역 확인&lt;/li&gt;
&lt;li&gt;listings, favorites, profiles, admins 테이블 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 인증은 아직 수업에서 배우지 않았기 때문에 이번 프로젝트에서는 Supabase가 제공하는 인증 기능을 활용하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 기능을 살리는 방향으로 개선하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 새로운 서비스를 만드는 것보다 현재 프로토타입을 기반으로 작은 기능을 하나씩 추가하는 방향이 현실적이라고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논의한 개선 아이디어는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즐겨찾기 여부를 색상이나 메시지로 표시&lt;/li&gt;
&lt;li&gt;공고 이미지와 원문 링크 제공&lt;/li&gt;
&lt;li&gt;회원가입 시 관심 지역과 개인 조건 입력&lt;/li&gt;
&lt;li&gt;자격 조건을 입력창 대신 체크박스나 선택 상자로 구성&lt;/li&gt;
&lt;li&gt;마감된 공고 처리&lt;/li&gt;
&lt;li&gt;즐겨찾기 순위와 사용자별 조회 기능 개선&lt;/li&gt;
&lt;li&gt;사용자의 조건에 맞는 공고 추천&lt;/li&gt;
&lt;li&gt;공고 댓글 기능 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 모두 구현한 것은 아니며, 각자 맡을 기능의 범위를 정하기 위한 아이디어로 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일반 기능과 AI 기능 구분하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 중요하게 고민한 부분은 &lt;b&gt;어떤 기능에 AI가 필요한가&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 DB 기능AI를 활용하기 좋은 기능&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;선택한 지역의 공고만 조회&lt;/td&gt;
&lt;td&gt;자연어로 조건을 입력해 공고 찾기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;신혼부부 공고 필터링&lt;/td&gt;
&lt;td&gt;복잡한 자격 조건을 쉬운 말로 설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;즐겨찾기 순위 계산&lt;/td&gt;
&lt;td&gt;비슷한 지역이나 대안 공고 추천&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마감일이 지난 공고 처리&lt;/td&gt;
&lt;td&gt;사용자 상황에 맞춘 공고 상담&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보증금과 월세 계산&lt;/td&gt;
&lt;td&gt;이전 대화를 기억하는 연속 상담&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 때 선택한 조건으로 공고를 필터링하는 것은 Supabase 쿼리만으로도 구현할 수 있다. 반면 &amp;ldquo;내 상황에 맞는 공고를 알려줘&amp;rdquo;처럼 자연어를 이해하고 이유까지 설명해야 한다면 Gemini와 같은 AI 모델이 더 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Gemini가 Supabase의 데이터를 자동으로 알고 있는 것은 아니다. 백엔드에서 관련 공고를 조회한 뒤 그 데이터를 프롬프트에 넣어 전달하거나, 이후 벡터 DB를 이용한 검색 구조를 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 첫 번째 목표는 &lt;b&gt;기본 챗봇을 완성하는 것&lt;/b&gt;으로 정하고, 시간이 남으면 다음 기능을 추가하기로 했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최근 대화 내용 기억&lt;/li&gt;
&lt;li&gt;Redis를 이용한 대화 또는 캐시 관리&lt;/li&gt;
&lt;li&gt;사용자 정보 기반 맞춤 추천&lt;/li&gt;
&lt;li&gt;벡터 DB를 활용한 공고 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 기능의 전체 흐름 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 기능 하나를 수정하더라도 화면만 바꾸면 끝나는 것이 아니었다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Supabase 테이블
&amp;rarr; FastAPI 스키마&amp;middot;서비스&amp;middot;라우터
&amp;rarr; API 클라이언트
&amp;rarr; 관리자&amp;middot;사용자 Streamlit 화면
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 자격 조건을 체크박스로 변경하면 DB 컬럼, 백엔드 API, 관리자 등록 화면, 사용자 조회 화면을 모두 함께 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프론트엔드와 백엔드를 완전히 나누기보다, &lt;b&gt;각자 하나의 기능을 맡아 DB부터 UI까지 전체 흐름을 구현하는 방식&lt;/b&gt;이 이번 프로젝트에 더 적합하다고 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Streamlit 화면이 느린 이유 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토타입을 사용하면서 버튼이나 체크박스를 누를 때 화면 전환이 느린 부분도 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능한 원인으로는 다음과 같은 항목을 살펴봤다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Streamlit의 화면 재실행 방식&lt;/li&gt;
&lt;li&gt;반복되는 백엔드 API 요청&lt;/li&gt;
&lt;li&gt;Supabase 조회 시간&lt;/li&gt;
&lt;li&gt;같은 데이터를 계속 다시 불러오는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 캐시를 사용하면 반복 조회를 줄일 수 있지만, 현재는 데이터와 사용자가 많지 않다. 따라서 무조건 Redis를 추가하기보다 &lt;b&gt;어느 구간이 실제로 느린지 먼저 확인하는 것이 우선&lt;/b&gt;이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 많아졌을 때는 DB 인덱스를 이용해 모든 데이터를 처음부터 확인하지 않고 필요한 위치를 빠르게 찾을 수 있다는 점도 함께 복습했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안전한 협업을 위한 규칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supabase 테이블을 여러 명이 함께 수정하면 실수로 컬럼이나 테이블을 삭제할 위험이 있다. 실제 공고 데이터를 많이 입력한 뒤 테이블이 삭제되면 복구하기 어려우므로 다음과 같은 규칙이 필요했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블이나 컬럼을 삭제하기 전에 팀원에게 알리기&lt;/li&gt;
&lt;li&gt;가능하면 삭제보다 추가와 수정을 우선하기&lt;/li&gt;
&lt;li&gt;테이블 생성 SQL과 샘플 데이터를 백업하기&lt;/li&gt;
&lt;li&gt;개인 브랜치에서 작업하기&lt;/li&gt;
&lt;li&gt;작은 기능 단위로 자주 병합하기&lt;/li&gt;
&lt;li&gt;수정한 기능과 이유를 기록하기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 단순히 기능 아이디어를 많이 내는 것보다 &lt;b&gt;정해진 시간 안에 완성할 수 있는 최소 기능을 먼저 만드는 것이 중요하다&lt;/b&gt;는 점을 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 추천이라는 이름이 붙었다고 모두 AI 기능은 아니었다. 조건이 명확하면 DB 필터링으로 해결할 수 있고, 자연어 이해와 설명이 필요할 때 AI를 사용하는 것이 적절했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 월요일까지 각자 맡은 핵심 기능을 완성하고, 화요일에는 기능 통합과 문서&amp;middot;발표 준비를 진행할 예정이다. 앞으로는 새로운 기술을 무작정 추가하기보다 현재 서비스에 꼭 필요한지 먼저 판단하며 프로젝트를 진행해야겠다.&lt;/p&gt;</description>
      <category>AI 오케스트레이션 캠프/일차별 회고</category>
      <category>ai국비지원</category>
      <category>ai오케스트레이션</category>
      <category>FastAPI</category>
      <category>Gemini</category>
      <category>github</category>
      <category>gitrebase</category>
      <category>streamlit</category>
      <category>supabase</category>
      <category>공공주택서비스</category>
      <category>미니프로젝트</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/244</guid>
      <comments>https://laycoder.tistory.com/244#entry244comment</comments>
      <pubDate>Thu, 6 Aug 2026 18:16:41 +0900</pubDate>
    </item>
    <item>
      <title>19일차 회고｜권한별 화면 구성&amp;middot;공공주택 MVP 기획｜AI 오케스트레이션 개발자 국비지원</title>
      <link>https://laycoder.tistory.com/243</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 기존에 만든 회원가입&amp;middot;로그인&amp;middot;상품 관리 기능을 바탕으로 일반 사용자와 관리자의 화면을 구분했다. 이후 팀원들과 미니 프로젝트 주제를 논의하며, 짧은 개발 기간 안에 완성할 수 있는 MVP 범위를 정했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자와 관리자 화면 구분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 데이터베이스를 사용하더라도 로그인한 사용자의 역할에 따라 보여주는 화면과 사용할 수 있는 기능이 달라야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 사용자는 상품 정보를 조회할 수 있지만 등록&amp;middot;수정&amp;middot;삭제 기능은 사용할 수 없도록 구성했다. 반면 관리자는 상품을 조회하는 것뿐만 아니라 새로운 상품을 등록하고 기존 정보를 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입 후 로그인&lt;/li&gt;
&lt;li&gt;로그인한 사용자 정보 확인&lt;/li&gt;
&lt;li&gt;일반 사용자와 관리자 화면 구분&lt;/li&gt;
&lt;li&gt;일반 사용자는 상품 조회&lt;/li&gt;
&lt;li&gt;관리자는 상품 등록&amp;middot;조회&amp;middot;수정&amp;middot;삭제&lt;/li&gt;
&lt;li&gt;로그아웃하면 로그인 상태 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 데이터를 사용하더라도 사용자용 페이지와 관리자용 페이지가 서로 다른 역할을 한다는 점을 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 교체하는 과정에서 이전에 만들었던 사용자 화면이 보이지 않는 문제도 있었다. 강사님 코드를 적용하면서 기존 파일이 바뀐 것으로 예상되어, 현재 실행 중인 프런트엔드와 파일 구성을 다시 확인했다. 여러 앱을 함께 실행할 때는 어떤 파일과 실행 경로를 사용하고 있는지 정확하게 구분해야 한다는 점을 배웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미니 프로젝트 주제 검토&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 후반에는 팀원들과 미니 프로젝트 주제를 논의했다. 여행 지원금 안내, AI 냉장고, 공공주택 공고 서비스 등 여러 아이디어를 검토한 결과, 서울의 청년&amp;middot;공공주택 공고를 한곳에서 확인할 수 있는 서비스를 유력한 주제로 정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 핵심 기능은 다음과 같이 정리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입과 로그인&lt;/li&gt;
&lt;li&gt;주택 공고 목록 조회&lt;/li&gt;
&lt;li&gt;공고의 제목&amp;middot;지역&amp;middot;설명&amp;middot;원문 링크 제공&lt;/li&gt;
&lt;li&gt;관심 있는 공고 찜하기&lt;/li&gt;
&lt;li&gt;마이페이지에서 찜한 공고 조회 및 삭제&lt;/li&gt;
&lt;li&gt;관리자 페이지에서 공고 관리&lt;/li&gt;
&lt;li&gt;공고별 찜 수와 인기 공고 차트 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 실습한 Streamlit&amp;middot;FastAPI&amp;middot;Supabase 구조를 뼈대로 활용하고, 상품 데이터를 주택 공고 데이터로 변경하면 기본 프로토타입을 빠르게 만들 수 있을 것으로 판단했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공공데이터 사용이 어려운 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공공주택 공고는 PDF나 표 형태로 제공되는 경우가 많고, 신청 자격도 1인 가구&amp;middot;신혼부부&amp;middot;고령자&amp;middot;수급자 등으로 복잡하게 나뉜다. 따라서 실제 공고 전체를 가져와 서비스에 맞는 데이터로 가공하려면 예상보다 많은 시간이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 처음부터 모든 공고를 처리하지 않고 약 10개의 공고만 준비하기로 했다. 데이터 항목도 제목&amp;middot;지역&amp;middot;간단한 설명&amp;middot;원문 링크처럼 꼭 필요한 정보부터 사용한다. 실제 데이터를 정리하기 어렵다면 먼저 목업 데이터를 넣어 기능을 완성한 후 교체하는 방법도 검토했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 서비스 개발에서는 기능 구현뿐만 아니라 데이터를 수집하고 정리하는 과정도 큰 작업이라는 점을 알게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RAG와 벡터 DB 검토&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 논의 중에는 AI가 공공주택 자료를 참고해 답변하는 RAG 기능도 검토했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG는 크게 세 단계로 이해할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Retrieve&lt;/b&gt;: 데이터베이스나 문서에서 질문과 관련된 정보를 찾는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Augment&lt;/b&gt;: 찾은 정보를 AI에게 보낼 질문에 추가한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Generate&lt;/b&gt;: AI가 제공받은 정보를 바탕으로 답변을 만든다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 데이터베이스가 ID나 조건을 기준으로 데이터를 찾는다면, 벡터 DB는 문장의 의미를 숫자로 변환해 비슷한 의미의 자료를 찾는다. 예를 들어 &amp;ldquo;청년주택에 입주하고 싶다&amp;rdquo;라는 질문과 의미상 가까운 공고를 검색하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 RAG를 제대로 구현하려면 문서 전처리, 임베딩, 벡터 DB, 검색 결과 연결 과정이 추가로 필요하다. 현재 프로젝트 기간을 고려하면 구현 난도가 높기 때문에, 우선은 Supabase에 저장된 공고를 조건에 따라 조회하는 기능부터 완성하기로 했다. AI 기능을 억지로 추가하기보다 실제로 작동하는 핵심 기능을 먼저 만드는 것이 중요하다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 역시 임시 데이터 저장, 캐시, 실시간 찜 수 관리 등에 사용할 수 있지만 프로젝트에 꼭 필요한지 확인한 뒤 적용하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애자일 방식으로 MVP 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 팀 논의에서 가장 중요했던 부분은 완벽한 계획을 세운 뒤 개발을 시작하는 것이 아니라, 작은 프로토타입을 먼저 만들어 가능성을 확인하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 기존 프로젝트에서 불필요한 날씨 조회와 서버 확인 기능 등을 제거하고 다음 기능만 남기기로 했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인&lt;/li&gt;
&lt;li&gt;공고 조회&lt;/li&gt;
&lt;li&gt;찜하기&lt;/li&gt;
&lt;li&gt;찜한 공고 모아보기&lt;/li&gt;
&lt;li&gt;관리자용 공고 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능들이 연결되는지 먼저 확인한 다음 시간이 남으면 검색 조건, 차트, 인기 공고 등의 기능을 추가한다. 다음 날 점심까지 1차 프로토타입을 만들어 팀원들과 확인하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 단순히 화면을 만드는 것에서 벗어나 사용자의 역할에 따라 기능을 구분하는 방법을 생각해 볼 수 있었다. 또한 좋은 아이디어라도 데이터 확보와 개발 기간을 고려하지 않으면 완성하기 어렵다는 점을 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 RAG나 벡터 DB처럼 새로운 기술을 넣는 것보다 회원가입, 로그인, 조회, 찜하기처럼 서비스의 핵심 흐름을 먼저 완성하는 것이 중요하다는 점이 기억에 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 사용자와 공고 사이의 찜 데이터를 어떤 테이블 구조로 연결할지 정리하고, 기존 상품 관리 코드를 공공주택 공고 서비스에 맞게 변경해 봐야겠다.&lt;/p&gt;</description>
      <category>AI 오케스트레이션 캠프/일차별 회고</category>
      <category>AI오케스트레이션개발자국비지원</category>
      <category>FastAPI</category>
      <category>mvp개발</category>
      <category>Rag</category>
      <category>streamlit</category>
      <category>supabase</category>
      <category>공공주택서비스</category>
      <category>권한별페이지</category>
      <category>미니프로젝트</category>
      <category>엔코아AI캠퍼스</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/243</guid>
      <comments>https://laycoder.tistory.com/243#entry243comment</comments>
      <pubDate>Wed, 5 Aug 2026 18:49:59 +0900</pubDate>
    </item>
    <item>
      <title>18일차 회고｜이미지 업로드&amp;middot;비동기 처리&amp;middot;상품 조회｜AI 오케스트레이션 개발자 국비지원</title>
      <link>https://laycoder.tistory.com/242</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지 파일과 상품 정보를 함께 다뤄본 하루&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 FastAPI와 Streamlit을 이용해 상품 이미지를 업로드하고, 이미지 정보를 상품 데이터와 함께 Supabase에 저장하는 과정을 실습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 상품명, 가격, 설명처럼 문자나 숫자로 이루어진 데이터가 중심이었다. 오늘은 여기에 이미지 파일이 추가되면서 처리 과정이 조금 더 복잡해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지는 데이터베이스에 그대로 넣는 것이 아니라 파일 시스템에 먼저 저장하고, 데이터베이스에는 이미지에 접근하기 위한 URL과 파일명을 기록해야 했다. 이를 통해 하나의 상품을 등록할 때 파일 저장과 데이터베이스 저장이 어떻게 연결되는지 이해할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 학습 내용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FastAPI에서 이미지 파일 받기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 등록 API에서는 상품명, 가격, 설명과 함께 이미지 파일도 전달받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 JSON 데이터와 달리 파일을 함께 전송하려면 Form, File, UploadFile을 사용해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Form: 상품명, 가격, 설명과 같은 입력값을 받는다.&lt;/li&gt;
&lt;li&gt;File: 해당 값이 파일이라는 것을 FastAPI에 알려준다.&lt;/li&gt;
&lt;li&gt;UploadFile: 업로드된 파일의 이름과 내용 등을 다룰 수 있게 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지는 선택적으로 등록할 수 있도록 구성했다. 이미지가 전달되지 않으면 이미지 URL과 파일명에 None을 사용해 이미지가 없는 상품도 등록할 수 있게 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 파일과 DB 정보 분리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 실습에서 중요했던 부분은 이미지 파일과 상품 데이터를 서로 다른 위치에 저장한다는 점이었다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;사용자가 상품 정보와 이미지 입력
&amp;rarr; FastAPI 라우터에서 요청 수신
&amp;rarr; 이미지 파일을 uploads 폴더에 저장
&amp;rarr; 이미지 URL과 파일명 생성
&amp;rarr; 상품 정보에 이미지 정보 추가
&amp;rarr; Supabase items 테이블에 저장
&amp;rarr; 등록 결과 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 이미지 파일은 uploads 폴더에 저장하고, Supabase에는 다음과 같은 상품 정보가 들어가도록 구성했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품명&lt;/li&gt;
&lt;li&gt;가격&lt;/li&gt;
&lt;li&gt;설명&lt;/li&gt;
&lt;li&gt;이미지 URL&lt;/li&gt;
&lt;li&gt;이미지 파일명&lt;/li&gt;
&lt;li&gt;등록 날짜&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 자체를 데이터베이스에 넣는 것이 아니라 파일의 위치를 찾을 수 있는 정보를 저장한다는 점을 새롭게 이해했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ItemCreate 모델에 이미지 정보 담기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우터에서는 입력받은 상품 정보와 이미지 저장 결과를 이용해 ItemCreate 객체를 새로 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지가 정상적으로 저장되면 반환받은 image_url과 image_filename을 상품명, 가격, 설명과 함께 모델에 담았다. 이후 완성된 상품 데이터를 서비스 계층으로 전달해 Supabase에 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 팀 프로젝트의 공통 모델을 임의로 변경하면 다른 코드와 설계에도 영향을 줄 수 있다는 점을 배웠다. 공통 스키마를 수정해야 한다면 개인이 바로 바꾸기보다 팀원들과 먼저 논의해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;async와 await를 사용하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 저장하는 함수에서는 async와 await가 사용됐다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;async def create_item(...):
    image_url, image_filename = await save_image(image)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async는 기다리는 작업이 포함된 비동기 함수라는 뜻이고, await는 해당 작업이 끝날 때까지 기다리면서 실행 순서를 다른 작업에 넘기는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 저장처럼 시간이 걸릴 수 있는 작업을 일반적인 동기 방식으로 처리하면, 해당 작업이 끝날 때까지 서버가 다른 요청을 처리하기 어려울 수 있다. 비동기 처리를 적용하면 이미지가 저장되는 동안에도 서버가 다른 사용자의 요청을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식당 직원이 음식 하나가 완성될 때까지 가만히 기다리는 것이 아니라, 기다리는 동안 다른 주문을 처리하는 것과 비슷하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 계층에서 Supabase에 저장하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 정보까지 담긴 ItemCreate 객체는 서비스 계층으로 전달됐다. 서비스에서는 상품 데이터를 Supabase의 items 테이블에 저장하고, 저장 결과를 ItemPublic 모델로 검증해 반환했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 입력 모델에서는 설명 필드가 desc, 데이터베이스에서는 description으로 사용되고 있어 서비스 계층에서 서로 연결해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답은 ApiResponse를 사용해 다음과 같이 일정한 구조를 유지했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 성공 여부&lt;/li&gt;
&lt;li&gt;사용자에게 보여줄 메시지&lt;/li&gt;
&lt;li&gt;실제 상품 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 오류와 입력값 검증 오류도 공통 핸들러를 이용해 비슷한 형태로 반환하도록 구성했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 오류와 해결 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메인 애플리케이션에 라우터 등록하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 등록 API를 작성했더라도 main.py에 라우터를 등록하지 않으면 Swagger에서 해당 API가 나타나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 item_router를 불러온 뒤 다음과 같은 방식으로 애플리케이션에 연결해야 했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;app.include_router(item_router)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우터, 서비스, 스키마 파일을 만드는 것만으로 API가 자동 등록되는 것이 아니라, 최종적으로 메인 애플리케이션과 연결해야 한다는 점을 다시 확인했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복사한 코드에 남아 있던 이름 수정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Product 코드를 참고해 Item 기능을 만들면서 이전 코드의 이름이 일부 남아 있어 import 오류가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 존재하지 않는 이미지 관련 함수나 이전 product 모듈의 이름이 남아 있으면 Uvicorn이 정상적으로 실행되지 않았다. 오류 메시지를 확인하면서 잘못된 import를 삭제하고, 관련 이름을 item 구조에 맞게 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 복사해 사용하는 것은 빠르게 기능을 만들 수 있다는 장점이 있지만, 이전 모듈명과 함수명이 남아 있지 않은지 반드시 확인해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VS Code 단축키 충돌 해결하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 중 VS Code의 한 줄 삭제 단축키인 Ctrl + Shift + K가 작동하지 않고 Notion이 실행되는 문제도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code의 키보드 로그를 확인한 결과 K 키 자체는 정상적으로 인식됐지만, 조합 단축키는 Notion Desktop이 먼저 가져가고 있었다. Notion의 명령어 검색 단축키가 같은 키로 설정된 것이 원인이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notion에서 명령어 검색 기능을 끄거나 다른 단축키로 변경해 프로그램 간 단축키 충돌을 해결할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상품 조회 화면 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 등록 기능에 이어 Supabase에 저장된 상품을 조회하는 화면도 구현하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 화면에는 다음 정보가 나타나도록 계획했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품명&lt;/li&gt;
&lt;li&gt;상품 가격&lt;/li&gt;
&lt;li&gt;상품 설명&lt;/li&gt;
&lt;li&gt;상품 이미지&lt;/li&gt;
&lt;li&gt;상품 등록 날짜&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 수정과 삭제 기능까지 확장하지 않고 조회 기능에 집중했다. Supabase에서 상품 정보를 가져오고, 저장된 이미지 정보를 이용해 Streamlit 화면에 이미지를 출력하는 흐름을 연습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit에서 이미지를 보여주는 기능은 이전에 학습했지만, 이번에는 데이터베이스의 상품 정보와 파일 시스템의 이미지를 연결해야 한다는 차이가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 오늘 확인된 범위에서는 상품 조회 페이지 구현을 시작한 단계이므로, 완성했다고 단정하기보다 이후 실제 코드와 실행 결과를 다시 점검할 필요가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가로 복습할 부분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 설정 파일에는 최대 이미지 크기와 허용할 이미지 형식에 관한 값도 준비돼 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대 이미지 크기: 5MB&lt;/li&gt;
&lt;li&gt;허용 이미지 형식&lt;/li&gt;
&lt;li&gt;이미지 업로드 경로&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 현재 확인한 코드에서는 이 설정을 이용한 실제 검증 로직까지 완성되지는 않았다. 이후에는 다음 기능을 추가해볼 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;허용하지 않는 이미지 형식 차단&lt;/li&gt;
&lt;li&gt;최대 용량을 초과한 파일 차단&lt;/li&gt;
&lt;li&gt;같은 이름의 파일이 올라왔을 때 파일명 중복 방지&lt;/li&gt;
&lt;li&gt;잘못된 파일명이나 경로 처리&lt;/li&gt;
&lt;li&gt;상품 삭제 시 연결된 이미지 파일도 함께 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정값을 만드는 것과 그 설정을 실제 검증 과정에서 사용하는 것은 별개의 작업이라는 점도 기억해야겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 단순한 CRUD에서 한 단계 더 나아가 파일 업로드가 포함된 상품 등록 과정을 경험했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 이미지 하나를 추가했을 뿐인데도 라우터에서 파일을 받고, 이미지 저장 함수를 기다리고, 반환된 정보를 모델에 담고, 서비스에서 데이터베이스에 저장하는 여러 과정이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 파일 저장과 데이터베이스 저장이 한 번에 이루어지는 것처럼 느껴졌지만, 실제로는 각각 다른 작업이며 코드가 두 작업을 순서대로 연결하고 있다는 것을 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 async와 await도 단순히 FastAPI 함수 앞에 붙이는 문법이 아니라, 파일 저장처럼 기다림이 발생하는 동안 서버가 다른 요청을 처리할 수 있게 도와주는 기능이라는 점이 조금 더 명확해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 상품 조회 화면을 완성하고, 이미지 형식과 용량을 실제로 검증하는 과정까지 이어서 복습해봐야겠다.&lt;/p&gt;</description>
      <category>AI 오케스트레이션 캠프/일차별 회고</category>
      <category>ai오케스트레이션</category>
      <category>FastAPI</category>
      <category>Python공부</category>
      <category>sk네트웍스</category>
      <category>streamlit</category>
      <category>supabase</category>
      <category>국비지원개발자과정</category>
      <category>비동기처리</category>
      <category>엔코아AI캠퍼스</category>
      <category>이미지업로드</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/242</guid>
      <comments>https://laycoder.tistory.com/242#entry242comment</comments>
      <pubDate>Tue, 4 Aug 2026 17:49:32 +0900</pubDate>
    </item>
    <item>
      <title>17일차 회고｜회원 인증&amp;middot;ERD와 Supabase 협업｜AI 오케스트레이션 개발자 국비지원</title>
      <link>https://laycoder.tistory.com/241</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인 기능에서 팀 프로젝트 설계까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 기존에 만들었던 FastAPI&amp;middot;Streamlit&amp;middot;Supabase 기반 CRUD 기능을 확장하고, 새로운 쇼핑몰 팀 프로젝트를 준비했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞부분에서는 회원 인증과 회원 정보 수정, 비밀번호 변경, 로그인 상태에 따른 화면 구성을 점검했다. 이후에는 팀원들과 쇼핑몰 프로젝트의 주제와 기능을 정하고, ERD와 데이터베이스 테이블, 역할 분담, GitHub 협업 방식 등을 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 코드를 많이 작성하는 것보다 팀원 모두가 같은 구조와 기준으로 개발할 수 있도록 프로젝트의 기반을 만드는 데 집중한 하루였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회원 인증과 로그인 상태 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 회원가입&amp;middot;로그인&amp;middot;로그아웃과 같은 회원 인증 기능을 다시 점검했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Streamlit 화면에서는 로그인 여부에 따라 사용자에게 보여주는 메뉴가 달라져야 한다. 로그인하지 않은 상태에서는 회원가입과 로그인 메뉴가 필요하고, 로그인한 이후에는 회원 정보 수정이나 로그아웃과 같은 기능을 이용할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 회원 정보와 비밀번호를 수정하는 기능을 통해 프론트엔드에서 입력한 데이터가 FastAPI를 거쳐 Supabase까지 전달되는 흐름을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Streamlit 입력
&amp;rarr; API Client 요청
&amp;rarr; FastAPI 처리
&amp;rarr; Supabase 데이터 수정
&amp;rarr; 결과를 Streamlit 화면에 표시
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 하나만 보면 단순해 보이지만, 로그인 상태와 입력값 검증, API 응답, 오류 메시지까지 함께 생각해야 자연스러운 화면을 만들 수 있다는 점을 배웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상품 관리 화면 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 만들었던 상품 CRUD 기능도 다시 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 관리 화면에서는 다음 기능이 하나의 흐름으로 작동하도록 구성했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 등록&lt;/li&gt;
&lt;li&gt;상품 전체 조회&lt;/li&gt;
&lt;li&gt;상품 한 개 조회&lt;/li&gt;
&lt;li&gt;상품 정보 수정&lt;/li&gt;
&lt;li&gt;상품 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 관리 기능은 이번 쇼핑몰 프로젝트에서도 참고할 수 있는 기본 예제가 됐다. 지금까지 만든 회원 인증&amp;middot;정보 수정 백엔드와 상품 관리 프론트엔드 코드는 Git에 커밋해 새로운 팀 프로젝트의 기반으로 활용하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쇼핑몰 팀 프로젝트 주제 선정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트의 주제는 기존에 구현한 회원과 상품 기능을 이어서 활용할 수 있는 쇼핑몰로 정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자 완전히 다른 주제의 테이블을 만들 수도 있었지만, 자동차&amp;middot;음악&amp;middot;책처럼 서로 관련 없는 기능이 섞이면 하나의 프로젝트로 보이기 어렵다. 따라서 처음에는 각 기능을 개별적으로 구현하더라도 나중에 연결할 수 있도록 하나의 쇼핑몰이라는 주제 안에서 기능을 나누기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 회원과 상품 기능에 다음 기능을 추가하기로 했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카테고리&lt;/li&gt;
&lt;li&gt;상품 리뷰&lt;/li&gt;
&lt;li&gt;상품 문의&lt;/li&gt;
&lt;li&gt;장바구니&lt;/li&gt;
&lt;li&gt;주문&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이 가운데 카테고리 CRUD와 PLAN.md, 디렉터리 규칙 정리를 담당하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능별 난이도가 다른 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능의 난이도는 CRUD 개수보다 다른 테이블과 얼마나 많이 연결되는지, 추가로 처리해야 하는 규칙이 얼마나 많은지에 따라 달라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능난이도주요 이유&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;카테고리&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;td&gt;이름을 중심으로 비교적 독립적인 CRUD 구현 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상품 리뷰&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;회원과 상품을 연결하고 작성자를 확인해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상품 문의&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;회원&amp;middot;상품 연결과 문의 상태&amp;middot;답변 처리가 필요함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장바구니&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;td&gt;회원별 조회, 상품 수량, 중복 상품 처리 등이 필요함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주문&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;td&gt;주문과 주문 상품을 분리하고 여러 테이블을 연결해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 주문은 하나의 테이블만으로 표현하기 어려워 orders와 order_items처럼 주문 정보와 주문 상품 정보를 나누는 구조를 검토했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장바구니도 단순히 데이터를 등록하는 것에서 끝나지 않는다. 같은 상품을 다시 담았을 때 수량을 늘릴지, 새로운 데이터를 만들지 정해야 하고, 로그인한 회원이 자신의 장바구니만 조회할 수 있도록 구성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 제한된 시간 안에 완성하는 것이 중요하기 때문에 실제 쇼핑몰의 모든 규칙을 구현하기보다, 우선 각 기능의 기본 CRUD를 완성한 뒤 시간이 남으면 관리자 기능과 테이블 간 연결을 확장하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ERD를 이용한 데이터베이스 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원들이 각자의 Supabase에 테이블을 따로 만드는 것이 아니라, 하나의 팀 프로젝트와 데이터베이스를 함께 사용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD는 데이터베이스의 테이블과 관계를 그림으로 나타낸 것이다. 어떤 테이블이 어떤 정보를 가지고 있고, 서로 어떤 키로 연결되는지 한눈에 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 상품 리뷰는 회원과 상품을 연결해야 한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;users
  └── product_reviews ── products
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 다른 테이블의 데이터를 가리키는 값을 외래 키(Foreign Key)라고 한다. 테이블끼리 외래 키로 연결하면 존재하지 않는 회원이나 상품에 리뷰가 작성되는 문제를 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀에서는 ERDCloud를 이용해 테이블 구조를 만들고, 생성된 SQL을 Supabase에 적용하는 방향으로 진행했다. 모든 테이블에는 기본적으로 다음 시간 정보를 포함하기로 했다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;created_at: 데이터가 처음 등록된 시간
updated_at: 데이터가 마지막으로 수정된 시간
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 데이터가 언제 생성됐고 마지막으로 언제 변경됐는지 확인할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PLAN.md와 공통 개발 규칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 시작하기 전에 PLAN.md에 프로젝트의 전체 계획을 정리하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계획서에는 다음 내용을 포함했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 주제와 목표&lt;/li&gt;
&lt;li&gt;핵심 기능&lt;/li&gt;
&lt;li&gt;테이블과 컬럼&lt;/li&gt;
&lt;li&gt;데이터 자료형&lt;/li&gt;
&lt;li&gt;테이블 간 관계&lt;/li&gt;
&lt;li&gt;CRUD 기능&lt;/li&gt;
&lt;li&gt;디렉터리와 파일 구조&lt;/li&gt;
&lt;li&gt;팀원별 담당 업무&lt;/li&gt;
&lt;li&gt;GitHub 브랜치와 코드 통합 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자 코드를 먼저 작성한 뒤 구조를 맞추면 파일 위치나 함수 이름이 달라져 통합 과정에서 문제가 생길 수 있다. 반대로 공통 구조를 먼저 정하면 담당 기능은 달라도 비슷한 방식으로 코드를 작성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 개발 외에도 GitHub 저장소 초기 설정, 코드 통합과 충돌 해결, ERD 및 테이블 생성, 문서 정리, 발표처럼 공통으로 필요한 업무를 나눴다. 난이도가 높은 주문 기능을 맡은 사람이 공통 작업까지 모두 담당하지 않도록 업무량도 함께 조정했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;협업 도구의 초대와 권한 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supabase&amp;middot;ERDCloud&amp;middot;Notion&amp;middot;GitHub를 함께 사용하면서 계정과 접근 권한 문제도 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 초대받은 이메일과 실제 서비스에 가입한 이메일이 다르면 초대된 프로젝트가 보이지 않거나 사용 권한이 없다는 메시지가 나타날 수 있었다. 네이버와 구글 계정을 혼용하면서 어떤 계정으로 가입했는지 혼란이 생기기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 다음 사항을 하나씩 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스에 가입한 이메일과 초대 이메일이 같은지 확인&lt;/li&gt;
&lt;li&gt;초대 메일을 수락했는지 확인&lt;/li&gt;
&lt;li&gt;팀 프로젝트 접근 권한 확인&lt;/li&gt;
&lt;li&gt;기존 조직과 불필요한 프로젝트 정리&lt;/li&gt;
&lt;li&gt;팀원 모두에게 동일한 프로젝트가 보이는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 데이터베이스의 접속 정보는 팀원 모두에게 필요하지만, Supabase 키가 들어 있는 .env 파일은 GitHub에 올리지 않고 각자의 개발 환경에 따로 설정해야 한다는 점도 중요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의 실습 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 팀 프로젝트에서는 기능 코드를 바로 작성하기보다 다음 준비 작업을 우선 진행했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쇼핑몰 프로젝트 주제 확정&lt;/li&gt;
&lt;li&gt;추가 CRUD 기능 선정&lt;/li&gt;
&lt;li&gt;팀원별 담당 기능 배정&lt;/li&gt;
&lt;li&gt;공통 작업 역할 분담&lt;/li&gt;
&lt;li&gt;ERD 작성&lt;/li&gt;
&lt;li&gt;테이블 생성 SQL 준비&lt;/li&gt;
&lt;li&gt;Supabase 팀 프로젝트 공유&lt;/li&gt;
&lt;li&gt;GitHub 저장소와 협업 방식 정리&lt;/li&gt;
&lt;li&gt;PLAN.md 및 디렉터리 규칙 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 회원과 상품 기능은 기본 코드로 활용하고, 새롭게 맡은 기능의 코드는 다음 작업부터 본격적으로 구현하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 실습에서는 CRUD 기능이 각각 독립적으로 보였지만, 쇼핑몰이라는 하나의 프로젝트로 묶어 보니 테이블의 관계와 업무 규칙이 훨씬 중요하게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리는 비교적 독립적이지만 리뷰는 회원과 상품이 필요하고, 장바구니와 주문은 여러 데이터와 처리 과정이 함께 움직여야 한다. 같은 CRUD라도 어떤 데이터와 연결되느냐에 따라 난이도가 크게 달라진다는 것을 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 팀 프로젝트에서는 코드를 빨리 작성하는 것만큼 공통 구조를 정하는 과정이 중요했다. 데이터베이스와 디렉터리 구조, 파일 이름, 역할 분담이 명확하지 않으면 각자 만든 코드를 합칠 때 더 큰 문제가 생길 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 내가 담당한 카테고리 CRUD를 공통 디렉터리 규칙에 맞춰 구현하고, FastAPI&amp;middot;Supabase&amp;middot;Streamlit이 정상적으로 연결되는지 테스트해야겠다. 이후에는 상품과 카테고리의 관계까지 확장해 쇼핑몰의 전체 구조 안에서 동작하도록 만들어보고 싶다.&lt;/p&gt;</description>
      <category>AI 오케스트레이션 캠프/일차별 회고</category>
      <category>ERD</category>
      <category>FastAPI</category>
      <category>GitHub협업</category>
      <category>SK네트웍스Family</category>
      <category>streamlit</category>
      <category>supabase</category>
      <category>국비지원개발자</category>
      <category>멀티AI에이전트</category>
      <category>쇼핑몰프로젝트</category>
      <category>엔코아AI캠퍼스</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/241</guid>
      <comments>https://laycoder.tistory.com/241#entry241comment</comments>
      <pubDate>Mon, 3 Aug 2026 18:10:24 +0900</pubDate>
    </item>
    <item>
      <title>SK네트웍스 Family 엔코아 AI 오케스트레이션 프리코스 4｜파이썬 반복문&amp;middot;함수&amp;middot;리스트 컴프리헨션&amp;middot;람다</title>
      <link>https://laycoder.tistory.com/240</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 기초 문법을 다루는 네 번째 프리코스에서는 반복되는 작업을 효율적으로 처리하는 방법과 코드를 기능별로 나누어 재사용하는 방법을 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 학습의 중심은 for문과 while문을 이용한 반복 처리, 함수의 정의와 호출, 파이썬이 기본으로 제공하는 내장 함수였다. 여기에 리스트 컴프리헨션과 람다 표현식까지 살펴보면서 짧고 효율적인 코드를 작성하는 방법도 함께 익혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 각각 별개의 문법처럼 느껴졌지만, 모두 코드의 중복을 줄이고 유지보수를 쉽게 만든다는 공통점이 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;반복문이 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 문장을 1,000번 출력해야 한다면 print()를 1,000줄 작성할 수도 있다. 하지만 출력할 문장을 수정해야 한다면 모든 줄을 하나씩 변경해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 같은 작업을 여러 번 실행해야 할 때 사용하는 것이 반복문이다. 반복문을 활용하면 반복 횟수나 조건만 지정하여 같은 코드 블록을 여러 번 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬에서 대표적으로 사용하는 반복문은 다음 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;for문: 반복할 대상이나 횟수가 정해져 있을 때 사용&lt;/li&gt;
&lt;li&gt;while문: 특정 조건이 참인 동안 계속 반복할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;for문과 range() 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for문은 문자열, 리스트, 튜플처럼 여러 요소를 가진 데이터에서 값을 하나씩 꺼내 반복해서 처리한다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;for number in range(5):
    print(number)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0
1
2
3
4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;range(5)는 0부터 4까지의 정수를 차례로 만들어 준다. 괄호 안에 작성한 5는 포함되지 않는다는 점이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;range()는 다음과 같은 형태로도 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;range(시작값, 끝값, 증감값)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 2부터 시작해 100보다 작은 짝수를 만들고 싶다면 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;numbers = list(range(2, 100, 2))
print(numbers)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;range()의 기본 시작값은 0이고 기본 증감값은 1이다. 끝값은 반드시 작성해야 하며, 작성한 끝값은 결과에 포함되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 range 객체는 리스트처럼 연속된 값을 다룰 수 있지만, 처음부터 모든 값을 메모리에 저장하는 리스트와는 차이가 있다. 필요한 시점에 값을 하나씩 만들어 사용하기 때문에 큰 범위를 반복할 때 메모리를 비교적 효율적으로 사용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이터러블과 이터레이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for문을 이해하려면 이터러블이라는 개념도 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이터러블(iterable)은 값을 한 번에 하나씩 꺼낼 수 있는 반복 가능한 객체다. 지금까지 배운 자료형 중에서는 다음과 같은 것들이 해당한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자열&lt;/li&gt;
&lt;li&gt;리스트&lt;/li&gt;
&lt;li&gt;튜플&lt;/li&gt;
&lt;li&gt;딕셔너리&lt;/li&gt;
&lt;li&gt;집합&lt;/li&gt;
&lt;li&gt;range 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;fruits = [&quot;apple&quot;, &quot;banana&quot;, &quot;orange&quot;]

for fruit in fruits:
    print(fruit)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for문은 fruits에서 값을 하나씩 꺼내 fruit 변수에 저장한다. 그리고 값을 하나 꺼낼 때마다 들여쓰기 된 코드 블록을 한 번씩 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 반복 가능한 객체에서 값을 하나씩 꺼내 처리하는 과정을 이터레이션이라고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;while문과 조건 반복&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;while문은 오른쪽에 작성한 조건식이 True인 동안 코드 블록을 계속 실행한다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;count = 0

while count &amp;lt; 5:
    print(count)
    count += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;count &amp;lt; 5가 참인지 확인한다.&lt;/li&gt;
&lt;li&gt;조건이 참이면 들여쓰기 된 코드를 실행한다.&lt;/li&gt;
&lt;li&gt;코드 실행이 끝나면 다시 조건식으로 돌아간다.&lt;/li&gt;
&lt;li&gt;조건이 거짓이 되면 반복을 종료한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;while문을 사용할 때 가장 주의해야 할 것은 무한 반복이다. 반복문 안에서 조건에 사용하는 값을 변경하지 않으면 조건이 계속 참으로 남을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;count = 0

while count &amp;lt; 5:
    print(count)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 count가 계속 0이므로 반복이 끝나지 않는다. 따라서 반복문 안에서 count += 1과 같이 조건이 언젠가 거짓이 되도록 값을 변경해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;break와 continue&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문 안에서는 break와 continue를 사용해 반복의 흐름을 조절할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;break는 현재 반복문을 즉시 종료한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;for number in range(10):
    if number == 5:
        break

    print(number)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 숫자가 5가 되는 순간 반복문을 종료하기 때문에 0부터 4까지만 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;continue는 현재 반복만 건너뛰고 다음 반복으로 이동한다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;for number in range(5):
    if number == 2:
        continue

    print(number)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 2를 출력하는 과정만 건너뛰기 때문에 0, 1, 3, 4가 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문에서 아무 작업도 작성하지 않고 문법의 형태만 먼저 만들어 두고 싶다면 pass를 사용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;for number in range(5):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;함수가 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 특정 작업을 수행하는 코드를 하나의 이름으로 묶어 놓은 것이다. 반복되는 코드를 함수로 만들면 필요할 때마다 함수의 이름을 호출하여 같은 작업을 다시 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반복되는 코드를 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;기능별로 코드를 나눌 수 있다.&lt;/li&gt;
&lt;li&gt;수정해야 할 부분을 쉽게 찾을 수 있다.&lt;/li&gt;
&lt;li&gt;같은 기능을 여러 곳에서 재사용할 수 있다.&lt;/li&gt;
&lt;li&gt;프로그램의 유지보수가 편해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬에서는 def 키워드로 함수를 정의한다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def add(a, b):
    return a + b
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 각 부분의 역할은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;def: 함수를 정의하는 키워드&lt;/li&gt;
&lt;li&gt;add: 함수 이름&lt;/li&gt;
&lt;li&gt;a, b: 함수가 값을 전달받는 매개변수&lt;/li&gt;
&lt;li&gt;return: 연산 결과를 함수 바깥으로 반환하는 키워드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정의한 함수는 함수 이름 뒤에 소괄호를 붙여 호출한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;result = add(5, 3)
print(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5는 a에 전달되고 3은 b에 전달된다. 함수 내부에서 a + b가 계산되고, 그 결과인 8이 return을 통해 함수가 호출된 자리로 돌아온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사칙연산 함수 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 함수의 정의와 호출 과정을 이해하기 위해 덧셈, 뺄셈, 곱셈, 나눗셈 함수를 예제로 살펴봤다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def add(a, b):
    &quot;&quot;&quot;두 개의 숫자를 더한 값을 반환합니다.&quot;&quot;&quot;
    return a + b


def subtract(a, b):
    &quot;&quot;&quot;두 개의 숫자를 뺀 값을 반환합니다.&quot;&quot;&quot;
    return a - b


def multiply(a, b):
    &quot;&quot;&quot;두 개의 숫자를 곱한 값을 반환합니다.&quot;&quot;&quot;
    return a * b


def divide(a, b):
    &quot;&quot;&quot;두 개의 숫자를 나눈 값을 반환합니다.&quot;&quot;&quot;
    return a / b
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 정의만 해서는 실행되지 않는다. 필요한 값을 전달하면서 직접 호출해야 한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;add_result = add(5, 3)
subtract_result = subtract(5, 3)
multiply_result = multiply(a=5, b=3)
divide_result = divide(10, b=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수에 값을 전달하는 방법은 크게 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 인자: 작성된 순서에 따라 값을 전달&lt;/li&gt;
&lt;li&gt;키워드 인자: 매개변수의 이름을 지정하여 값을 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식을 함께 사용한다면 위치 인자를 먼저 작성하고, 그다음에 키워드 인자를 작성해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;독스트링과 help() 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 안의 첫 번째 줄에 삼중 따옴표를 사용하면 함수에 대한 설명을 작성할 수 있다. 이를 독스트링(docstring)이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def add(a, b):
    &quot;&quot;&quot;두 개의 숫자를 더한 값을 반환합니다.&quot;&quot;&quot;
    return a + b
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 독스트링은 help()를 이용해 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;help(add)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;__doc__ 속성으로도 독스트링만 직접 조회할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;print(add.__doc__)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 만든 함수뿐 아니라 파이썬 내장 함수의 사용법도 같은 방식으로 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;help(len)
help(print)
help(max)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수의 사용법이 기억나지 않을 때마다 인터넷에서 검색하기보다 help()를 먼저 활용하는 습관을 들이면 좋을 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파이썬 내장 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장 함수는 파이썬 인터프리터에 기본으로 포함되어 있어 별도의 모듈을 불러오지 않아도 바로 사용할 수 있는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 사용했던 print(), input(), len()도 모두 내장 함수에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서 살펴본 주요 내장 함수는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장 함수역할&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;int()&lt;/td&gt;
&lt;td&gt;값을 정수형으로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;str()&lt;/td&gt;
&lt;td&gt;값을 문자열로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list()&lt;/td&gt;
&lt;td&gt;값을 리스트로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tuple()&lt;/td&gt;
&lt;td&gt;값을 튜플로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set()&lt;/td&gt;
&lt;td&gt;값을 집합으로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bool()&lt;/td&gt;
&lt;td&gt;값을 불리언으로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max()&lt;/td&gt;
&lt;td&gt;가장 큰 값을 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min()&lt;/td&gt;
&lt;td&gt;가장 작은 값을 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;abs()&lt;/td&gt;
&lt;td&gt;절댓값을 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sum()&lt;/td&gt;
&lt;td&gt;여러 숫자의 합계를 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;len()&lt;/td&gt;
&lt;td&gt;객체의 길이 또는 요소 개수를 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type()&lt;/td&gt;
&lt;td&gt;값의 자료형을 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;range()&lt;/td&gt;
&lt;td&gt;일정한 범위의 정수를 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;round()&lt;/td&gt;
&lt;td&gt;숫자를 반올림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;print()&lt;/td&gt;
&lt;td&gt;값을 화면에 출력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;input()&lt;/td&gt;
&lt;td&gt;사용자에게 값을 입력받음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;help()&lt;/td&gt;
&lt;td&gt;함수나 객체의 사용법을 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;map()&lt;/td&gt;
&lt;td&gt;각 요소에 같은 함수를 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;filter()&lt;/td&gt;
&lt;td&gt;조건에 맞는 요소만 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 내장 함수를 잘 활용하면 직접 모든 기능을 처음부터 만들지 않아도 되기 때문에 코드를 더욱 빠르고 간결하게 작성할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리스트 컴프리헨션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 컴프리헨션은 반복 가능한 객체에서 값을 하나씩 꺼내 새로운 리스트를 만드는 문법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 for문으로 리스트를 만들면 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;numbers = []

for number in range(1, 10):
    numbers.append(number)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 컴프리헨션을 사용하면 같은 작업을 한 줄로 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;numbers = [number for number in range(1, 10)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[표현식 for 변수 in 반복할_객체]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문에서 값을 하나씩 꺼낸 다음, 앞쪽의 표현식을 계산한 결과가 새로운 리스트의 요소로 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건식을 추가하면 필요한 값만 선택할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;even_numbers = [
    number
    for number in range(10)
    if number % 2 == 0
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 0부터 9까지의 숫자 중 짝수만 리스트에 담는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현식에서 값을 가공하는 것도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;squared_even_numbers = [
    number ** 2
    for number in range(10)
    if number % 2 == 0
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[0, 4, 16, 36, 64]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 컴프리헨션은 리스트를 간결하게 생성할 때 유용하지만, 반복문과 조건이 여러 번 중첩되면 오히려 읽기 어려워질 수 있다. 코드가 복잡해진다면 한 줄로 줄이는 것보다 일반적인 for문을 사용하는 편이 가독성에 더 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 리스트를 만드는 것이 목적일 때 사용하는 문법이므로, 단순히 값을 출력하려고 표현식 자리에 print()를 작성하는 것은 적절하지 않다. print()는 결과로 None을 반환하기 때문에 None만 담긴 리스트가 만들어질 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;람다 표현식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다는 이름 없이 한 줄로 작성하는 간단한 함수다. 그래서 익명 함수라고도 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ldif&quot;&gt;&lt;code&gt;lambda 매개변수: 표현식
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자를 두 배로 만드는 람다 표현식은 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;lambda number: number * 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다 표현식을 변수에 저장하면 일반 함수처럼 호출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;twice = lambda number: number * 2

print(twice(10))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다는 한 줄의 표현식으로 작성할 수 있는 간단한 작업에 적합하다. 여러 줄의 코드가 필요하거나 중간 변수를 만들어야 한다면 def를 사용해 일반 함수로 작성하는 편이 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;map()과 람다 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map()은 반복 가능한 객체의 각 요소에 같은 함수를 적용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;numbers = [1, 2, 3, 4, 5]

result = map(
    lambda number: number * 2,
    numbers,
)

print(list(result))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[2, 4, 6, 8, 10]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map()은 결과를 리스트가 아닌 map 객체로 반환한다. 따라서 결과를 리스트 형태로 확인하려면 list()로 변환해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개 이상의 반복 가능한 객체를 함께 전달할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;a = [0, 1, 2, 3, 4, 5]
b = [0, 2, 4, 6, 8, 10]

result = map(
    lambda x, y: x + y,
    a,
    b,
)

print(list(result))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 리스트의 같은 위치에 있는 값이 차례로 x와 y에 전달되고, 두 값을 더한 결과가 새로운 요소로 만들어진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;filter()와 람다 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter()는 반복 가능한 객체에서 조건을 만족하는 요소만 골라낸다. 첫 번째 인자로 전달하는 함수는 각 요소를 검사한 후 True 또는 False를 반환해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;numbers = [3, 8, 20, 10, 7, 0, 9]

result = filter(
    lambda number: 0 &amp;lt; number &amp;lt; 10 and number % 3 == 0,
    numbers,
)

print(list(result))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 0보다 크고 10보다 작으면서 3으로 나누어떨어지는 숫자만 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map()은 각 요소를 변환할 때 사용하고, filter()는 조건에 맞는 요소를 선택할 때 사용한다는 차이가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;짧은 코드보다 읽기 쉬운 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 여러 작업을 한 줄로 줄이는 문법을 배웠지만, 무조건 짧은 코드가 좋은 코드는 아니라는 점도 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 컴프리헨션이나 람다 표현식은 간단한 작업에서는 편리하다. 하지만 조건문과 반복문이 여러 번 중첩되면 코드를 이해하기 어려워질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 작성할 때는 다음 기준을 함께 고려해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 사람이 코드를 쉽게 이해할 수 있는가?&lt;/li&gt;
&lt;li&gt;나중에 다시 봐도 동작을 파악할 수 있는가?&lt;/li&gt;
&lt;li&gt;수정해야 할 부분을 쉽게 찾을 수 있는가?&lt;/li&gt;
&lt;li&gt;같은 코드가 불필요하게 반복되고 있지는 않은가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로 줄이는 것보다 기능을 명확하게 전달하고 유지보수하기 쉬운 코드를 작성하는 것이 더 중요하다는 점을 배울 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 학습의 핵심 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프리코스에서는 반복문부터 함수, 리스트 컴프리헨션, 람다 표현식까지 파이썬 코드를 효율적으로 작성하는 데 필요한 문법을 폭넓게 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for문은 반복 가능한 객체에서 값을 하나씩 꺼내 처리하고, while문은 조건이 참인 동안 작업을 반복한다. 함수는 반복되는 코드를 하나의 기능으로 분리하여 필요할 때마다 재사용하게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬이 제공하는 내장 함수를 이용하면 자주 필요한 기능을 직접 만들지 않고 바로 사용할 수 있다. help()와 독스트링을 활용하면 함수의 사용법과 목적도 코드 안에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 컴프리헨션과 람다는 간단한 반복 처리와 함수를 짧게 표현할 수 있는 편리한 문법이었다. 다만 코드가 복잡해질 때는 일반적인 반복문과 함수로 풀어 작성하는 것이 더 읽기 쉽다는 점도 함께 기억해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 문법 하나하나를 익히는 데 집중했다면, 이번 학습부터는 중복을 줄이고 코드를 구조적으로 작성하는 방법을 배우기 시작한 느낌이었다. 앞으로 직접 여러 예제를 작성하면서 반복문의 흐름, 함수의 반환값, 위치&amp;middot;키워드 인자, 리스트 컴프리헨션과 람다의 적절한 사용 시점을 익혀야겠다.&lt;/p&gt;</description>
      <category>AI 오케스트레이션 프리코스</category>
      <category>ai오케스트레이션</category>
      <category>AI오케스트레이션프리코스</category>
      <category>sk네트웍스</category>
      <category>내장함수</category>
      <category>리스트컴프리헨션</category>
      <category>엔코아AI캠퍼스</category>
      <category>파이썬기초</category>
      <category>파이썬람다</category>
      <category>파이썬반복문</category>
      <category>파이썬함수</category>
      <author>랩보다 AI 더 잘해지기</author>
      <guid isPermaLink="true">https://laycoder.tistory.com/240</guid>
      <comments>https://laycoder.tistory.com/240#entry240comment</comments>
      <pubDate>Mon, 3 Aug 2026 09:45:18 +0900</pubDate>
    </item>
  </channel>
</rss>