<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>yundlab 님의 블로그</title>
    <link>https://yundlab.tistory.com/</link>
    <description>yundlab 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 5 Aug 2026 18:46:51 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yundlab</managingEditor>
    <image>
      <title>yundlab 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8508818/attach/1fe1da6fd85843eb8cc450b1ce601e50</url>
      <link>https://yundlab.tistory.com</link>
    </image>
    <item>
      <title>[260803 25일차] 프로젝트 #6</title>
      <link>https://yundlab.tistory.com/32</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKeCha/dJMcaaTKXkG/WC1OFSuyMvh3CYaCFoqAW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKeCha/dJMcaaTKXkG/WC1OFSuyMvh3CYaCFoqAW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKeCha/dJMcaaTKXkG/WC1OFSuyMvh3CYaCFoqAW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKeCha%2FdJMcaaTKXkG%2FWC1OFSuyMvh3CYaCFoqAW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;

&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# 포트폴리오 학습일지&lt;/span&gt;
        &lt;h1&gt;[프로젝트 6일차] 2주차 시작 · 화면 설계&lt;br&gt;+ 요구사항·ERD 정합성 검토 · Pulse UI 조립&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.08.03 (월) · 2주차 킥오프(화면 목록·와이어프레임) + 문서 정합성 검토 + 개인 UI 작업(Figma)&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;2주차 첫날. 팀은 &lt;strong&gt;화면 목록·와이어프레임·디자인&lt;/strong&gt; 작업으로 넘어갔고, 협업 규칙(PR 템플릿·&lt;strong&gt;Squash and Merge&lt;/strong&gt;)을 레포에 적용했다. 기획서 기준으로 &lt;strong&gt;요구사항 명세서·ERD 정합성을 검토&lt;/strong&gt;해 모순·누락 항목을 정리했고, 개인적으로는 실시간 피드백 서비스 &lt;strong&gt;Pulse&lt;/strong&gt;의 디자인 토큰부터 화면 조립까지를 Figma로 마무리해 갔다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;2주차 범위&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;화면 목록&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;와이어프레임&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;디자인&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;협업 적용&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;PR 템플릿&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;Squash Merge&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;문서 검토&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;요구사항·ERD&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;정합성&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;FE 역할&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;3축 재편&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;내 작업&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;디자인 토큰&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;컴포넌트&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;화면 조립&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;미니 프로젝트&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;코드 학습 도구&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오전 공지 — 2주차 킥오프&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;금주 범위&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;화면 목록 / 와이어프레임 / 디자인 작업 + 프론트엔드 작업 분배(팀 R&amp;R 참조)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;PR 템플릿&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;팀원이 공유한 템플릿으로 레포에 적용&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;머지 정책&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;&lt;strong&gt;Squash and Merge로 확정&lt;/strong&gt; — GitHub 관련 설정도 함께 적용&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;회의록&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;오전 11:30 회의록 작성해 Notion 업로드 (플러그인으로 자동화)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;Notion 초대&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;새로 만든 E조 페이지에 각자 개인 계정을 게스트로 초대(강사님 포함), 산출물은 &quot;6. 산출물 관리&quot; 섹션으로 이동&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;오늘/내일까지 결정할 항목&lt;/strong&gt;&lt;br&gt;
    · (오늘) &lt;strong&gt;전원&lt;/strong&gt; — 산출물(요구사항 명세서·데이터 모델링)의 &quot;회의에서 결정할 것&quot; 섹션에 의견 남기기&lt;br&gt;
    · (내일) &lt;strong&gt;FE 담당자&lt;/strong&gt; — 프론트엔드 각자 파트 분담 아이디어를 Slack/Notion에 공유&lt;/div&gt;
    &lt;p style=&quot;font-size:14px; color:#666;&quot;&gt;그 외 기타: 한국취업센터 1차 정규 상담이 팀원별 시간대로 배정됨 · AI API 지원 관련은 강사님 확인 후 공유 · 최종 발표 초청 희망 기업 조사(기한 미정).&lt;/p&gt;

    &lt;h2&gt; ‍  팀장 멘토링 (13:00~13:30)&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;미니 프로젝트&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;2주차 &quot;코드 학습 스터디 도구&quot; 아이디어가 주제에 부합('학습의 큰 범위')한다고 강사님 동의 — Claude 스킬/플러그인으로 구현해도 무방&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;교재&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;공유 예정 교재는 아직 검토 단계, 완료되면 공유&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;환경 세팅&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;기획서 기반 산출물·규칙을 &lt;strong&gt;이번 주 수요일까지 확정&lt;/strong&gt;하고 GitHub/CLAUDE.md 업데이트&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  프론트엔드 역할 분담 재편 — 3축 구조&lt;/h2&gt;
    &lt;p&gt;기존 분담안의 약점을 Claude Code 자문으로 다시 짰다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;구분&lt;/th&gt;&lt;th&gt;구조&lt;/th&gt;&lt;th&gt;문제/개선&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;기존&lt;/td&gt;&lt;td&gt;한 명이 &lt;strong&gt;실제 기능이 없는 공통 작업&lt;/strong&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;strong&gt;기능 기준 3축&lt;/strong&gt;으로 분할&lt;/td&gt;&lt;td&gt;모두가 실제 기능을 나눠 맡는 구조&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h2&gt;  요구사항 명세서 · ERD 정합성 검토&lt;/h2&gt;
    &lt;p&gt;기획서를 기준으로 요구사항 명세서·ERD에 이상이 없는지 Claude Code로 대조 검토했다. 세 문서의 관계를 이렇게 이해하면 쉽다.&lt;/p&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;기획서 · 요구사항 명세서 · ERD = 같은 서비스를 점점 자세히 그린 세 장의 그림&lt;/strong&gt;&lt;br&gt;
    · &lt;strong&gt;기획서&lt;/strong&gt;: &quot;우리 서비스가 무엇을 하는지&quot;의 큰 그림&lt;br&gt;
    · &lt;strong&gt;요구사항 명세서&lt;/strong&gt;: 그 그림을 &quot;화면에서 어떤 순서·조건으로 동작하는지&quot; 규칙으로 풀어쓴 것&lt;br&gt;
    · &lt;strong&gt;ERD&lt;/strong&gt;: 그 규칙을 지키려면 DB에 어떤 표(테이블)·칸(컬럼)이 필요한지 그린 것&lt;br&gt;
    뒤로 갈수록 앞 내용을 &lt;strong&gt;빠짐없이&lt;/strong&gt; 담고 있어야 한다. 검토는 세 문서를 나란히 놓고 대조해 서로 다르거나 사라진 부분을 찾는 작업이다.&lt;/div&gt;
    &lt;p&gt;발견한 문제는 세 종류로 나뉜다. &lt;strong&gt;  모순&lt;/strong&gt;(문서마다 다른 답) → &lt;strong&gt;  누락&lt;/strong&gt;(앞엔 있는데 뒤에서 사라짐) → &lt;strong&gt;  미확정&lt;/strong&gt;(규칙은 있으나 DB에 적용하면 앞뒤가 안 맞음) 순으로 우선순위. 이 중 &lt;strong&gt;1번·6번&lt;/strong&gt;이 구현에 가장 직접적 영향을 준다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;분류&lt;/th&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;핵심 문제&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;  모순&lt;/td&gt;&lt;td&gt;1. 감정 분석 실패 처리&lt;/td&gt;&lt;td&gt;실패 시 &lt;code&gt;NEU&lt;/code&gt;(중립) vs &lt;code&gt;UNKNOWN&lt;/code&gt;(판단 불가)이 절마다 다르고, 감정값이 3종 vs 4종으로 엇갈림. ERD는 옛 방식(NEU) 기준 → 진짜 중립 소감과 섞여 감정 비율·정확도 검증이 왜곡됨&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;  누락&lt;/td&gt;&lt;td&gt;2. 이벤트 수정/삭제 규칙&lt;/td&gt;&lt;td&gt;기획서엔 이벤트 CRUD·삭제 권한이 있는데 요구사항엔 생성·시작/종료까지만. ERD엔 삭제 시 딸린 세션·소감 처리 규칙 없음&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;  누락&lt;/td&gt;&lt;td&gt;3. 공유 링크·QR 생성 시점&lt;/td&gt;&lt;td&gt;기획서엔 필수인데 요구사항엔 설명 없음 — 생성 시점(생성 즉시/LIVE 전환 시)·저장 여부 미정&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;  누락&lt;/td&gt;&lt;td&gt;4. 중복 제출 방지&lt;/td&gt;&lt;td&gt;기획서는 &quot;결정 필요&quot;로 표시했으나 요구사항엔 언급 없음. 반면 실시간 조회 절은 &quot;제출 여부 추적&quot;을 이미 전제로 함&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;  누락&lt;/td&gt;&lt;td&gt;5. 어뷰징 제출 횟수&lt;/td&gt;&lt;td&gt;&quot;IP/브라우저당 분당 &lt;code&gt;N&lt;/code&gt;회&quot;의 &lt;code&gt;N&lt;/code&gt;이 빈칸 — 구현 시점에 즉흥 결정될 위험&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;  누락&lt;/td&gt;&lt;td&gt;6. 태깅 버전 기록&lt;/td&gt;&lt;td&gt;&lt;code&gt;taggerVersion&lt;/code&gt;을 항상 전송·없으면 거절한다면서, 정작 저장할 칸이 요구사항·ERD 어디에도 없음 → 받고 버려서 이력 추적 불가&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;  미확정&lt;/td&gt;&lt;td&gt;7. 리포트 미생성 상태&lt;/td&gt;&lt;td&gt;ERD상 리포트 없으면 행 자체가 없는데, 요구사항은 &quot;미생성&quot;을 상태 기본값으로 설명 → 행 선생성 vs 버튼 시 생성 결정 필요&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;  미확정&lt;/td&gt;&lt;td&gt;8. 세션 삭제 조건 충돌&lt;/td&gt;&lt;td&gt;&quot;소감 없을 때만 세션 삭제&quot; 규칙 + 소감은 소프트 삭제 → 한 번이라도 제출됐다 지워진 세션은 영영 못 지울 수 있음&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;배운 점&lt;/strong&gt; — 같은 서비스를 그린 문서인데도 절이 늘어나고 시점이 달라지면 조용히 어긋난다. 특히 &quot;실패했을 때 어떤 값으로 저장하나&quot;, &quot;이 데이터를 어디에 남기나&quot; 같은 &lt;strong&gt;예외·저장 규칙&lt;/strong&gt;이 문서 사이에서 가장 잘 사라진다. 구현 전에 세 문서를 대조하는 것만으로 나중의 재작업을 크게 줄일 수 있다.&lt;/div&gt;

    &lt;h2&gt;  내가 한 일 — Pulse: 디자인 토큰에서 화면 조립까지&lt;/h2&gt;
    &lt;p&gt;실시간 이벤트 피드백 서비스 &lt;strong&gt;Pulse&lt;/strong&gt;의 UI 작업. Foundations(색·타이포·간격 토큰) 등록을 끝낸 시점부터 컴포넌트를 만들고 화면을 조립하기까지의 과정과, 그 사이 내린 판단들을 정리했다.&lt;/p&gt;

    &lt;h3&gt;1. 시작점의 착각 — &quot;컴포넌트가 너무 적은데?&quot;&lt;/h3&gt;
    &lt;p&gt;토큰 등록 후 컴포넌트를 만들기 시작했을 때, 이전 프로젝트(Button·Chip·Modal·Toast… 수십 개)와 비교하며 다섯 개뿐이라 불안했다. 결론은 &lt;strong&gt;적은 게 아니라 순서가 다른 것&lt;/strong&gt;이었다. 컴포넌트는 화면을 만들기 전에 다 예측해 채우는 게 아니라, 화면을 조립하다 &quot;어, 이거 또 나오네&quot; 싶을 때 뽑아 올리는 게 정상 흐름이다.&lt;/p&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;기준&lt;/strong&gt; — 지금 만들 화면 하나에 필요한 것만 먼저 만든다. 참가자 제출 화면 기준으로 보니 Chip은 있는데 정작 &lt;strong&gt;Textarea가 없었고&lt;/strong&gt;, 감정 배지는 대시보드용이라 이 화면엔 안 쓰였다. &quot;많고 적음&quot;이 아니라 &lt;strong&gt;다음 화면에 필요한가&lt;/strong&gt;가 판단 기준. 조립하며 Header·Banner·Hint·Toast·Stat·Thermometer·FeedItem·Alert·Logo 등이 그때그때 추가됐다.&lt;/div&gt;

    &lt;h3&gt;2. 만든 컴포넌트&lt;/h3&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;컴포넌트&lt;/th&gt;&lt;th&gt;형태&lt;/th&gt;&lt;th&gt;비고&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Button&lt;/td&gt;&lt;td&gt;H 52/48/36, radius 8&lt;/td&gt;&lt;td&gt;primary / secondary / disabled&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Chip&lt;/td&gt;&lt;td&gt;H 32, radius 999&lt;/td&gt;&lt;td&gt;default / selected&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Badge&lt;/td&gt;&lt;td&gt;H 24, radius 999&lt;/td&gt;&lt;td&gt;positive / neutral / negative / toxic / none&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Field/Input&lt;/td&gt;&lt;td&gt;H 48, radius 8&lt;/td&gt;&lt;td&gt;placeholder 속성&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Field/Textarea&lt;/td&gt;&lt;td&gt;H 96, radius 8&lt;/td&gt;&lt;td&gt;소감·설명 입력&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Header/Mobile · Desktop&lt;/td&gt;&lt;td&gt;H 56 / 64&lt;/td&gt;&lt;td&gt;로고 / 로고+계정 슬롯&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Banner · Toast&lt;/td&gt;&lt;td&gt;padding 14/12 · 16/12&lt;/td&gt;&lt;td&gt;상태 배너 / 짧은 알림&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Stat · FeedItem&lt;/td&gt;&lt;td&gt;padding 16/14 · 14/12&lt;/td&gt;&lt;td&gt;label+value / normal·flagged&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Alert&lt;/td&gt;&lt;td&gt;W 360, padding 24&lt;/td&gt;&lt;td&gt;title+description&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Logo/Lockup&lt;/td&gt;&lt;td&gt;Hug&lt;/td&gt;&lt;td&gt;마크 + 워드마크&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;토큰 고정값&lt;/strong&gt; — radius: 버튼·입력·박스 8 / 카드 12 / 칩·배지·온도계 999. 간격: 라벨↔요소 4, 칩·버튼 8, 카드 12, 폼 필드 16, 섹션 24, 큰 블록 32. 컨트롤 높이: 헤더 56·64, 입력 48, 버튼 52·48·36, 칩 32, 배지 24. 그리드: 모바일 393(4컬럼·마진20·거터16), 데스크톱 1440(12컬럼·마진80·거터24).&lt;/div&gt;

    &lt;h3&gt;3. 완성한 화면&lt;/h3&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;대상&lt;/th&gt;&lt;th&gt;화면&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;참가자 (모바일 393)&lt;/td&gt;&lt;td&gt;소감 제출 &lt;code&gt;/e/{code}&lt;/code&gt; · 종료된 이벤트 · 실시간 반응 &lt;code&gt;/live&lt;/code&gt; · 공개 리포트 &lt;code&gt;/report&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;주최자 (데스크톱 1440)&lt;/td&gt;&lt;td&gt;로그인/회원가입 · 이벤트 목록 &lt;code&gt;/dashboard&lt;/code&gt; · 이벤트 편집(기본/세션 추가·삭제 확인/이벤트 삭제 확인) · 실시간 대시보드(LIVE/QR 모달/복사 토스트/종료 확인) · 대시보드 ENDED(요약 전/생성 중/완료)&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h3&gt;4. 하면서 내린 판단들&lt;/h3&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;리포트는 모바일 우선 + SSR&lt;/strong&gt; — 처음엔 데스크톱으로 그렸지만, 참가자 &quot;종료된 이벤트&quot; 화면에 &quot;결과 리포트 보기&quot; 버튼이 있어 되돌렸다. 리포트는 주최자 전용이 아니라 &lt;strong&gt;로그인 없이 열리는 공개 페이지&lt;/strong&gt;라 모바일 기준. 또한 이 프로젝트에서 SSR이 실제로 값하는 유일한 화면이다(대시보드=CSR, 제출=단순). SEO·OG·빠른 첫 로드가 의미 있는 건 공유되는 리포트뿐.&lt;/div&gt;
    &lt;p&gt;&lt;strong&gt;카드 클릭 → 상태별로 다른 곳으로.&lt;/strong&gt; 이벤트 목록에 &quot;수정&quot; 버튼을 따로 두지 않고 카드 전체를 클릭 가능하게 해 상태별로 목적지를 나눴다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;상태&lt;/th&gt;&lt;th&gt;이동 화면&lt;/th&gt;&lt;th&gt;이유&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;DRAFT&lt;/td&gt;&lt;td&gt;이벤트 편집&lt;/td&gt;&lt;td&gt;아직 시작 전이니 설정하러&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;LIVE&lt;/td&gt;&lt;td&gt;실시간 대시보드&lt;/td&gt;&lt;td&gt;진행 중이니 모니터링하러&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;ENDED&lt;/td&gt;&lt;td&gt;공개 리포트&lt;/td&gt;&lt;td&gt;끝났으니 결과 보러&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;p style=&quot;font-size:14px; color:#666;&quot;&gt;클릭 힌트로 카드 오른쪽 끝에 화살표만 붙였다(hover만으로는 알기 어려움). 덕분에 DRAFT 대시보드 화면 자체를 없앨 수 있었다 — DRAFT 카드는 편집 화면으로 가므로 그 화면에 들어올 경로가 없어서.&lt;/p&gt;
    &lt;p&gt;&lt;strong&gt;위험도에 따라 피드백 방식을 나눔.&lt;/strong&gt;&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;액션&lt;/th&gt;&lt;th&gt;방식&lt;/th&gt;&lt;th&gt;이유&lt;/th&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;tr&gt;&lt;td&gt;세션 삭제&lt;/td&gt;&lt;td&gt;Alert&lt;/td&gt;&lt;td&gt;되돌릴 수 없음&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;이벤트 삭제&lt;/td&gt;&lt;td&gt;Alert(문구 더 무겁게)&lt;/td&gt;&lt;td&gt;소감·리포트까지 사라짐&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;이벤트 종료&lt;/td&gt;&lt;td&gt;Alert(버튼 빨강 아님)&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;/table&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;빨강은 최종 확정 순간에만&lt;/strong&gt; — 삭제 버튼 위계를 두 단계로. 편집 화면의 &quot;삭제&quot;는 secondary(흰 배경)+빨간 글자로 왼쪽 끝에, Alert 안의 &quot;삭제&quot;만 primary+빨간 배경. 빨간 채움 버튼은 시선을 가장 먼저 끄는데, 편집 화면의 주요 액션은 &quot;저장/시작&quot;이지 삭제가 아니므로 삭제를 강조하면 위계가 뒤집힌다.&lt;/div&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;세션 추가는 모달 대신 인라인&lt;/strong&gt; — 세션은 필드가 제목 하나뿐이라 모달을 쓰면 Modal 컴포넌트가 더 필요. &quot;+ 세션 추가&quot;를 누르면 그 자리가 입력 필드+&quot;확인&quot; 버튼으로 바뀐다. 버튼 라벨을 &quot;추가&quot;가 아니라 &lt;strong&gt;&quot;확인&quot;&lt;/strong&gt;으로 한 건 수정 때도 같은 UI를 재사용하려고 — 추가는 빈 필드, 수정은 기존 제목이 채워진 필드, 그것만 다르다.&lt;/div&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;요약 생성은 자동이 아니라 수동&lt;/strong&gt; — 종료 시 자동 생성도 가능했지만 주최자가 버튼을 눌러야 생성되게 했다. LLM 무료 티어 레이트리밋 때문에 종료마다 자동 호출되면 발표날 한도에 걸릴 수 있어서. 상태는 LIVE(disabled)/ENDED 요약 전(활성)/생성 중(disabled)/완료(요약 카드) 네 개로. &quot;생성 시 공개 링크가 열려요&quot; 캡션은 실수로 참가자 소감이 공개되는 걸 막으려고 남겼다.&lt;/div&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;워드클라우드는 Figma와 코드가 다르다&lt;/strong&gt; — Figma에선 단어를 Absolute로 흩뜨려 회전을 줘야 자연스럽지만, 실제 데이터는 개수·빈도가 매번 달라 좌표를 미리 못 잡는다. 구현은 두 갈래: ① &lt;strong&gt;태그 클라우드&lt;/strong&gt;(flex-wrap + 폰트 크기만 빈도 비례, CSS만으로) ② &lt;strong&gt;진짜 패킹&lt;/strong&gt;(d3-cloud로 겹침 방지·나선 배치). 시안 느낌은 1번으로 충분하고, 2번은 리사이즈·SSR·번들 크기가 붙어 MVP엔 과해서 미뤘다.&lt;/div&gt;

    &lt;h3&gt;5. Figma 삽질 기록&lt;/h3&gt;
    &lt;p&gt;같은 오류를 여러 번 반복했는데 원인은 대부분 두 가지로 수렴했다.&lt;/p&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;① Fill 옵션이 안 보인다&lt;/strong&gt; — Fill은 부모가 오토레이아웃일 때만 나타난다. 컴포넌트 원본은 부모가 없으니 안 뜬다 → 원본은 폭을 숫자로 고정하고, 화면에 인스턴스로 배치할 때 Fill로 바꾼다. 부모가 Hug면 자식만 Fill로 걸어도 안 늘어난다(부모부터 Fill).&lt;br&gt;&lt;br&gt;
    &lt;strong&gt;② 요소가 엉뚱한 데 있다&lt;/strong&gt; — Absolute position이 켜져 있으면 오토레이아웃 흐름에서 빠져 좌표가 제멋대로가 된다(좌표가 마이너스거나 X/Y 직접 입력 가능하면 켜진 상태). 반대로 딤 배경처럼 화면 전체를 덮는 요소는 켜야 한다.&lt;/div&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;증상&lt;/th&gt;&lt;th&gt;원인 / 해결&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;요소가 안 보임&lt;/td&gt;&lt;td&gt;Opacity가 0%&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;라벨까지 가운데로 밀림&lt;/td&gt;&lt;td&gt;카드 전체에 중앙 정렬 → 가운데 정렬할 요소만 Fill 프레임으로 감싸기&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;텍스트 정렬이 안 먹음&lt;/td&gt;&lt;td&gt;텍스트가 Hug라 여백 없음 → 부모 프레임 정렬 변경&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;벡터 아이콘이 안 커짐&lt;/td&gt;&lt;td&gt;일반 리사이즈로는 패스가 안 바뀜 → Scale 툴(K)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;체크 표시 색이 안 바뀜&lt;/td&gt;&lt;td&gt;✔️는 이모지 → ✓(U+2713) 또는 SVG&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;도넛 조각이 세로로 쌓임&lt;/td&gt;&lt;td&gt;오토레이아웃 안에선 Align이 안 먹음 → 밖에서 겹치고 그룹으로 묶기&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;variant 충돌 경고&lt;/td&gt;&lt;td&gt;값 이름 겹침 → &lt;code&gt;type=primary&lt;/code&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;/table&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;Ellipse Arc 함정&lt;/strong&gt; — Sweep은 각도가 아니라 &lt;strong&gt;퍼센트&lt;/strong&gt;다. 58%를 넣으려고 208.8(=360×0.58)을 입력했다 한참 헤맸다. 그냥 &lt;strong&gt;58&lt;/strong&gt;이 맞다. Ratio(도넛 구멍)는 조각 세 개 모두 같은 값이어야 구멍이 안 어긋난다.&lt;/div&gt;

    &lt;h3&gt;6. 토큰을 정해놓고도 안 지키게 되는 순간&lt;/h3&gt;
    &lt;p&gt;작업 중반에 v2 HTML 목업 값을 그대로 옮기다 토큰에 없는 숫자를 심었다(상하 간격 18/20/24 제각각, 버튼 높이 44, 모더레이션 버튼 32). v2 목업은 &lt;strong&gt;토큰을 정하기 전 초안&lt;/strong&gt;이라 값이 서로 안 맞았는데, 그걸 정답처럼 참조한 게 문제였다.&lt;/p&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;교훈&lt;/strong&gt; — 기준은 나중에 등록한 토큰 쪽. 간격은 전부 24, 버튼은 48·36으로 다시 통일했다. 시안이 여러 버전일 때 &lt;strong&gt;어느 쪽이 기준인지 먼저 정해두지 않으면&lt;/strong&gt; 이런 일이 생긴다.&lt;/div&gt;

    &lt;h3&gt;7. 남은 일&lt;/h3&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;Prototype&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;카드 상태별 이동 경로를 실제 클릭으로 확인 가능하게 연결&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;데이터 모델&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;이벤트 날짜 필드 추가 여부 결정(현재 createdAt만 있음)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;컴포넌트 정리&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Section으로 묶어 이름표 노출&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;개발 착수&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;화면 완성 후 Contract-First 원칙에 따라 API 명세부터&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;2주차 첫날은 &quot;정리하고 넘어가는&quot; 날이었다. 협업 규칙을 레포에 붙였고, 문서 세 장을 대조해 어긋난 부분을 8건 찾아냈다. 특히 &lt;strong&gt;&quot;실패했을 때 어떤 값으로 저장하나&quot;&lt;/strong&gt; 같은 예외 규칙이 문서 사이에서 가장 잘 사라진다는 걸 배웠다. 개인 작업에서는 &lt;strong&gt;컴포넌트는 화면을 조립하며 뽑는 것&lt;/strong&gt;, &lt;strong&gt;빨강은 최종 확정에만&lt;/strong&gt;, &lt;strong&gt;요약은 레이트리밋 때문에 수동으로&lt;/strong&gt; 같은 판단들이 남았다. 화면을 다 완성하면 그다음은 Contract-First로 API 명세부터다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ 포트폴리오 &amp;nbsp;|&amp;nbsp; 화면 설계 &amp;nbsp;|&amp;nbsp; 요구사항·ERD 정합성 &amp;nbsp;|&amp;nbsp; Figma &amp;nbsp;|&amp;nbsp; 디자인 토큰 &amp;nbsp;|&amp;nbsp; Squash Merge &amp;nbsp;|&amp;nbsp; Pulse&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>ERD 정합성</category>
      <category>figma</category>
      <category>pulse</category>
      <category>squash merge</category>
      <category>디자인 토큰</category>
      <category>요구사항</category>
      <category>포트폴리오</category>
      <category>학습일지</category>
      <category>화면 설계</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/32</guid>
      <comments>https://yundlab.tistory.com/32#entry32comment</comments>
      <pubDate>Mon, 3 Aug 2026 16:16:48 +0900</pubDate>
    </item>
    <item>
      <title>[260731 24일차] 프로젝트 #5</title>
      <link>https://yundlab.tistory.com/31</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/54GG4/dJMcadpmsXp/7oQ8XsBQ4pTmZIpZXXHKn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/54GG4/dJMcadpmsXp/7oQ8XsBQ4pTmZIpZXXHKn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/54GG4/dJMcadpmsXp/7oQ8XsBQ4pTmZIpZXXHKn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F54GG4%2FdJMcadpmsXp%2F7oQ8XsBQ4pTmZIpZXXHKn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;

&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# 포트폴리오 학습일지&lt;/span&gt;
        &lt;h1&gt;[프로젝트 5일차] Figma UI · 머지 정책 확정&lt;br&gt;+ ERD id 타입 결정 · JWT 저장 전략&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.31 (금) · 개인 작업(디자인) + 오전 공지·팀장 멘토링 + 기술 결정(ERD·머지·JWT)&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;1주차 산출물 마무리 구간. 개인적으로는 &lt;strong&gt;Figma로 UI 작업&lt;/strong&gt;을 이어갔고, 팀은 &lt;strong&gt;PR 템플릿·머지 정책(Squash)&lt;/strong&gt;을 확정하고 &lt;strong&gt;ERD의 id 타입&lt;/strong&gt;을 투표로 정했다. 팀장 멘토링에서 &lt;strong&gt;주간 미니 프로젝트 주제(2~6주차)&lt;/strong&gt;와 Notion 워크스페이스 이슈가 공유됐고, 팀원이 학습한 &lt;strong&gt;JWT 저장 전략&lt;/strong&gt;도 정리해 뒀다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;내 작업&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Figma UI&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;스크립트&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;협업 확정&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;PR 템플릿&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;Squash merge&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;ERD 결정&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;id = bigint&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;email = 로그인 id&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;멘토링&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;발표 순서&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;주간 도구 주제&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;인프라&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Neon&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;Render 배포&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;개념 학습&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;JWT 저장&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;HttpOnly·CSRF&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  내가 한 일 — Figma UI&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;UI 작업&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Figma로 화면 UI 제작 (진행중) — 어제 잡은 디자인 시스템·컴포넌트 기반으로 실제 화면 구성&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;스크립트&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;시간이 되면 발표/시연 스크립트까지 작성 예정&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오전 공지 — 산출물·협업 규칙 확정&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;일일 Recap&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;팀원 recap을 취합해 Notion 팀 페이지 하단 별도 섹션에서 관리, 매일 오전 10시까지 공유&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;PR 템플릿&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;전날 팀원이 올린 &lt;code&gt;pull_request_template.md&lt;/code&gt;를 그대로 레포에 적용&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;머지 정책&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Squash vs Rebase → &lt;strong&gt;Squash로 확정&lt;/strong&gt; (어제 논의에서 Squash로 의견이 모였음)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;구분&lt;/th&gt;&lt;th&gt;내용&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;1주차 산출물(이번 주)&lt;/td&gt;&lt;td&gt;기획서 · 요구사항 명세서 · ERD · &lt;strong&gt;API 명세서 초안&lt;/strong&gt; + 문서별 필요한 개념/기술 학습&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;2주차 예고(다음 주)&lt;/td&gt;&lt;td&gt;화면 목록 · 와이어프레임 · 디자인 작업, FE 작업 분배(팀 R&amp;R 참조)&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;보안 메모&lt;/strong&gt; — 오후에 팀스페이스용 &lt;strong&gt;공용 Notion 계정 정보&lt;/strong&gt;가 공유됐다. 자격증명(이메일·비밀번호)은 학습일지에는 남기지 않는다. 공용 계정은 노출되면 팀 전체가 영향을 받으므로 Slack 등 공유 채널에서만 확인하고, 문서·블로그에는 기록하지 않는 게 원칙.&lt;/div&gt;

    &lt;h2&gt; ‍  팀장 멘토링 — 발표·주간 도구 주제&lt;/h2&gt;
    &lt;p&gt;팀장 멘토링에서 나온 내용 공유. 우리 팀(E팀)은 1주차 미니 프로젝트 &lt;strong&gt;발표 2번째 순서&lt;/strong&gt;로 정해졌다.&lt;/p&gt;
    &lt;h3&gt;주간 미니 프로젝트 주제&lt;/h3&gt;
    &lt;p&gt;각 도구는 &lt;strong&gt;우리 팀 프로젝트 안에 실제로 적용한 결과&lt;/strong&gt;를 내야 하고, Before/After가 정량 수치로 나오면 좋다.&lt;/p&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;2주차&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;코드 학습 스터디 도구 (예: &quot;이 도구를 쓰면 파이썬을 쉽게 배울 수 있다&quot;)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;3주차&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;클린 코드를 위한 도구 (컴파일러/인터프리터 제작 X — 유명 클린코드 규칙을 발췌·적용하는 방식도 무방)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;4주차&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;코드 리뷰 도구 (예: PR 리뷰 봇)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;5·6주차&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;자율 주제&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;그 외 멘토링 이슈&lt;/strong&gt;&lt;br&gt;
    ① &lt;strong&gt;Notion 팀스페이스&lt;/strong&gt; — 현재 수강생+강사가 모두 '멤버'라, 무료 블록을 다 쓰면 멤버마다 비용이 청구됨. 해결안은 팀장이 새 워크스페이스를 만들고 기존 페이지를 복제(복제 후에도 무료 블록이 충분한지 확인 필요).&lt;br&gt;
    ② &lt;strong&gt;AI API 지원금&lt;/strong&gt; — 성남시 최대 지원 금액을 강사님이 담당자에게 확인 예정.&lt;br&gt;
    ③ &lt;strong&gt;최종 발표 초청 희망 기업&lt;/strong&gt; — 원하는 기업이 있으면 자유롭게 공유(제출 기한 확인 예정).&lt;/div&gt;

    &lt;h2&gt;  기술 결정 — ERD id 타입 &amp; BE 머지 규칙&lt;/h2&gt;
    &lt;p&gt;엔티티를 짜면서 ERD를 다시 점검했다. 테이블 id를 &lt;strong&gt;UUID string&lt;/strong&gt;으로 할지 &lt;strong&gt;bigint number&lt;/strong&gt;로 할지가 쟁점이었다. (ERD에 &lt;code&gt;string&lt;/code&gt;으로 적혀 있던 건 Mermaid 기본값이 &lt;code&gt;id string&lt;/code&gt;이라 그렇게 나온 것.)&lt;/p&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;결정 — id는 &lt;code&gt;bigint&lt;/code&gt;&lt;/strong&gt; · 이유: 순차적으로 자동 생성이 가능해 다루기 편함. 투표 결과(Claude 의견 포함) 과반 동의로 확정, ERD의 &lt;code&gt;string&lt;/code&gt;도 &lt;code&gt;bigint&lt;/code&gt;로 수정. 부가로 &lt;strong&gt;로그인 시 email이 id 역할&lt;/strong&gt;을 하고, &lt;code&gt;userName&lt;/code&gt; 필드는 MVP에선 사소해 우선 넘어감(그라운드 룰에 따라 결정).&lt;/div&gt;
    &lt;h3&gt;BE 브랜치 머지 설정&lt;/h3&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;PR 필수&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;as-is 팀원 1명 승인 필수 → to-be 승인 수 0 (대신 CodeRabbit 리뷰 확인 + CI 통과로 대체)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;CodeRabbit&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;대화(conversation) 전부 resolve 필요&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;보호 규칙&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;force push / 브랜치 삭제 금지 (유지)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;비상 탈출구&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;admin 우회 가능은 유지&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;p style=&quot;font-size:14px; color:#666;&quot;&gt;인프라 쪽은 &lt;strong&gt;Neon(PostgreSQL) 연결 + Render 무료 호스팅&lt;/strong&gt;으로 스캐폴드 배포까지 진행, 엔티티 클래스는 &lt;strong&gt;package-by-feature&lt;/strong&gt; 구조로 작성 중.&lt;/p&gt;

    &lt;h2&gt;  개념 정리 — JWT를 어디에 저장할까 (HttpOnly 쿠키 vs 헤더 vs localStorage)&lt;/h2&gt;
    &lt;p&gt;팀원이 JWT 저장 전략을 파고들어 정리해 줬다. 핵심은 &lt;strong&gt;&quot;토큰을 어디에 두느냐&quot;&lt;/strong&gt;가 곧 보안 성격을 결정한다는 것.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;저장 방식&lt;/th&gt;&lt;th&gt;서버가 로그인 확인&lt;/th&gt;&lt;th&gt;주요 위험 / 특성&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;strong&gt;HttpOnly 쿠키&lt;/strong&gt; JWT&lt;/td&gt;&lt;td&gt;자동 (브라우저가 알아서 전송)&lt;/td&gt;&lt;td&gt;JS가 못 읽어 XSS 탈취에 강함 · 대신 &lt;strong&gt;CSRF 방어 필요&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;strong&gt;Authorization 헤더&lt;/strong&gt; JWT&lt;/td&gt;&lt;td&gt;가능, 단 JS가 매 요청마다 첨부해야 함&lt;/td&gt;&lt;td&gt;새로고침·SSR·미들웨어에서 번거로움 · JS가 토큰을 읽어 XSS 탈취 위험&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;strong&gt;localStorage&lt;/strong&gt; + 헤더&lt;/td&gt;&lt;td&gt;가능&lt;/td&gt;&lt;td&gt;XSS에 특히 취약 → 보통 &lt;strong&gt;비권장&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;핵심 정리&lt;/strong&gt; — 일반 브라우저 웹 서비스(React/Next/Vue)라면 권장 기본값은 &lt;strong&gt;access JWT를 HttpOnly + Secure + SameSite 쿠키&lt;/strong&gt;에 담고, 별도 &lt;strong&gt;CSRF 토큰&lt;/strong&gt;을 &lt;code&gt;X-CSRF-Token&lt;/code&gt; 헤더로 보내는 구성이다. &quot;헤더에 실으면 로그인 상태를 못 판단한다&quot;는 반만 맞는 말 — 서버는 &lt;code&gt;Authorization: Bearer&lt;/code&gt;로도 당연히 확인하지만, &lt;strong&gt;브라우저가 그 헤더를 자동으로 붙여주지 않는다는 것&lt;/strong&gt;이 차이다. 쿠키는 해당 도메인 요청에 자동 전송되므로 서버가 곧바로 인증할 수 있다.&lt;/div&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;왜 localStorage는 비권장?&lt;/strong&gt; — localStorage에 JWT를 넣으면 헤더 인증은 되지만, XSS 취약점이 &lt;strong&gt;한 번만&lt;/strong&gt; 있어도 악성 JS가 토큰을 읽어 외부로 빼낼 수 있다. HttpOnly 쿠키는 JS가 읽을 수 없어 이 &quot;토큰 탈취&quot; 위험을 크게 줄인다. 단, 모바일 앱·외부 공개 API·타 도메인 클라이언트가 주 구조라면 Authorization 헤더 방식이 더 자연스러울 수 있다.&lt;/div&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;Spring Boot / Spring Security 메모&lt;/strong&gt; — 원칙은 동일하다. JWT를 쿠키로 보내면 &lt;code&gt;SessionCreationPolicy.STATELESS&lt;/code&gt;여도 브라우저가 쿠키를 자동 전송하므로 CSRF 위험이 사라지지 않는다. 따라서 &lt;strong&gt;JWT 인증 필터 + CSRF 보호&lt;/strong&gt;를 함께 적용한다. 관례: 인증 JWT 쿠키는 &lt;code&gt;HttpOnly=true&lt;/code&gt;(JS 접근 차단), CSRF 토큰 쿠키(&lt;code&gt;XSRF-TOKEN&lt;/code&gt;)만 &lt;code&gt;HttpOnly=false&lt;/code&gt;로 두어 SPA가 읽어 &lt;code&gt;X-XSRF-TOKEN&lt;/code&gt; 헤더로 옮기게 한다. &lt;code&gt;__Host-&lt;/code&gt; 접두사를 쓰려면 &lt;code&gt;Secure&lt;/code&gt;·&lt;code&gt;Path=/&lt;/code&gt;·&lt;code&gt;Domain&lt;/code&gt; 미설정 조건이 필요.&lt;/div&gt;
    &lt;p style=&quot;font-size:13px; color:#888;&quot;&gt;※ 팀원 B는 이 외에 서버 액션·&lt;code&gt;zod&lt;/code&gt;도 학습했고, 상세 자료는 &lt;code&gt;jwt_저장_전략.md&lt;/code&gt;로 팀에 공유됨.&lt;/p&gt;

    &lt;h2&gt;  주간 미니 프로젝트 발표 — meeting-notes&lt;/h2&gt;
    &lt;p&gt;오늘 팀별로 1주차 미니 프로젝트를 발표했다. 우리 팀이 만든 건 &lt;strong&gt;meeting-notes&lt;/strong&gt; — 회의록과 액션 아이템을 Notion에 자동으로 등록해 주는 &lt;strong&gt;Claude Code 슬래시 커맨드 플러그인&lt;/strong&gt;이다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;내용&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;프로젝트 기간&lt;/td&gt;&lt;td&gt;2026-07-27(월) ~ 2026-07-30(목)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;프로젝트명&lt;/td&gt;&lt;td&gt;meeting-notes (Claude Code 슬래시 커맨드 플러그인)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;주제&lt;/td&gt;&lt;td&gt;Notion MCP 연동으로 회의록·액션 아이템을 자동 등록하는 Claude Code 플러그인&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;h3&gt;사용 기술&lt;/h3&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;실행 플랫폼&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Claude Code 플러그인(슬래시 커맨드) — 별도의 서버·프론트엔드 코드는 없음&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;외부 연동&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;&lt;code&gt;notion-team&lt;/code&gt; MCP 서버를 통한 Notion API 연동&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;설정 관리&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;&lt;code&gt;config.json&lt;/code&gt; 파일에 팀원 정보를 저장해 관리&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;  GitHub&lt;/strong&gt; — &lt;a href=&quot;https://github.com/ndopy/meeting-notes-plugin&quot;&gt;github.com/ndopy/meeting-notes-plugin&lt;/a&gt;&lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;1주차의 마지막 날답게 &quot;정하고 넘어가는&quot; 결정이 많았다. 머지 정책은 Squash로, ERD의 id는 bigint로 못을 박았고, PR 템플릿까지 레포에 붙였다. 개인적으로는 Figma가 조금씩 손에 익어 실제 화면을 만들기 시작했다. 특히 팀원이 정리해 준 &lt;strong&gt;JWT 저장 전략&lt;/strong&gt;이 인상 깊었다 — &quot;토큰을 어디 두느냐&quot;가 곧 XSS·CSRF 방어 설계로 이어진다는 걸 처음 구체적으로 이해했다. 다음 주 8/3부터 본격 구현이라, 이번 주에 잡아둔 문서·규칙이 그대로 뼈대가 된다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ 포트폴리오 &amp;nbsp;|&amp;nbsp; Figma &amp;nbsp;|&amp;nbsp; Squash merge &amp;nbsp;|&amp;nbsp; ERD &amp;nbsp;|&amp;nbsp; JWT &amp;nbsp;|&amp;nbsp; HttpOnly·CSRF &amp;nbsp;|&amp;nbsp; Neon·Render&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>ERD</category>
      <category>figma</category>
      <category>HttpOnly &amp;middot; CSRF</category>
      <category>JWT</category>
      <category>Neon &amp;middot; Render</category>
      <category>squash merge</category>
      <category>포트폴리오</category>
      <category>학습일지</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/31</guid>
      <comments>https://yundlab.tistory.com/31#entry31comment</comments>
      <pubDate>Mon, 3 Aug 2026 16:16:04 +0900</pubDate>
    </item>
    <item>
      <title>[260730 23일차] 프로젝트 #4</title>
      <link>https://yundlab.tistory.com/29</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zDV13/dJMcadiGtoD/uBgSKVgZY8rinT0cxDA8CK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zDV13/dJMcadiGtoD/uBgSKVgZY8rinT0cxDA8CK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zDV13/dJMcadiGtoD/uBgSKVgZY8rinT0cxDA8CK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzDV13%2FdJMcadiGtoD%2FuBgSKVgZY8rinT0cxDA8CK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;

&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# 포트폴리오 학습일지&lt;/span&gt;
        &lt;h1&gt;[프로젝트 4일차] 와이어프레임·디자인 시스템&lt;br&gt;+ Git 머지 정책 · 산출물 범위 확정&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.30 · 개인 작업(디자인) + 오전·오후 팀 회의(산출물 범위·머지 정책)&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;오늘은 개인적으로 &lt;strong&gt;와이어프레임·디자인 시스템·컴포넌트&lt;/strong&gt; 같은 화면 기반을 잡았고, 팀 회의에선 &lt;strong&gt;1주차 기획 산출물 범위&lt;/strong&gt;와 &lt;strong&gt;코드 작성 시작일(8/3)&lt;/strong&gt;을 확정했다. 오후엔 &lt;strong&gt;Git 머지 정책(Rebase vs Squash)&lt;/strong&gt;과 PR 템플릿을 논의했는데, 머지 전략 용어가 헷갈려서 이참에 개념을 정리했다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;내 작업&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;와이어프레임&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;디자인 시스템&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;컴포넌트&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;도구&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Figma&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;Figma→JSON&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;오전 회의&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;산출물 범위&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;8/3 코드 시작&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;머지 정책&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Rebase vs Squash&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;협업&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;PR 템플릿&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;Org Owner&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;미니 프로젝트&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;회의록 시스템&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;시연 영상&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  내가 한 일 — 화면 기반 잡기&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;와이어프레임/UI&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Figma로 작업(진행중) — 퍼블리싱은 늘 포토샵으로 해와서 아직 툴을 더듬더듬 익히는 중&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;디자인 시스템&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;색·타이포·간격 등 디자인 토큰 설정 (완료)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;컴포넌트 제작&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;공통 컴포넌트 제작 (완료)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;문서 숙지&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;기획서·요구사항 명세서 내용 정확히 숙지&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;왜 Figma로?&lt;/strong&gt; — UI를 다 잡아두면 &lt;strong&gt;Figma에서 JSON으로 변환&lt;/strong&gt;할 수 있고, 그 JSON을 Claude에게 주면 화면을 거의 똑같이 만들어 준다. 그래서 손으로 하나하나 퍼블리싱하는 대신, 디자인을 확정→JSON→구현으로 이어가려고 Figma를 익히는 중이다.&lt;/div&gt;

    &lt;h2&gt;  오전 회의 — 산출물 범위 &amp; 일정&lt;/h2&gt;
    &lt;p&gt;기획 산출물을 어디까지 이번 주에 낼지 확정했다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;구분&lt;/th&gt;&lt;th&gt;내용&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;1주차 산출물&lt;/td&gt;&lt;td&gt;기획서 · 요구사항 명세서 · &lt;strong&gt;ERD&lt;/strong&gt; · &lt;strong&gt;API 명세 초안&lt;/strong&gt;까지 마무리&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;2주차로 이월&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;strong&gt;전원 공동 작업&lt;/strong&gt;(각 문서 꼼꼼히 확인)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;실제 코드 시작&lt;/td&gt;&lt;td&gt;기획서 기준대로 &lt;strong&gt;8/3(월)&lt;/strong&gt;부터&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h2&gt;  오후 회의 — Git 머지 정책 &amp; PR 템플릿&lt;/h2&gt;
    &lt;p&gt;브랜치·PR은 정했는데, &lt;strong&gt;PR을 대상 브랜치에 어떻게 합칠지(머지 방식)&lt;/strong&gt;가 남아 논의했다. 팀 안에서 &quot;머지 커밋을 남기자&quot;, &quot;요즘은 Squash가 일반적이다&quot; 등 의견이 오갔고, Claude·GPT로 각 방식을 확인했다.&lt;/p&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;머지 방식&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Rebase vs Squash 중 택1 → 잠정 Squash, 내일 오전 10시까지 이견 없으면 Squash and merge로 확정&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;PR 템플릿&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;팀원마다 PR 서식이 달라 통일용 템플릿 도입 (필요 시 협의 수정)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;Org 권한&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;GitHub Organization 전원 Role을 Owner로 변경&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  개념 정리 — Merge 방식 3가지 (Merge / Squash / Rebase)&lt;/h2&gt;
    &lt;p&gt;PR을 &lt;code&gt;dev&lt;/code&gt;에 합칠 때 &lt;strong&gt;이력을 어떻게 남기느냐&lt;/strong&gt;가 세 방식의 차이다. 결과 코드는 같지만 &lt;strong&gt;커밋 기록의 모양&lt;/strong&gt;이 달라진다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;방식&lt;/th&gt;&lt;th&gt;대상 브랜치에 남는 이력&lt;/th&gt;&lt;th&gt;특징&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;strong&gt;Merge commit&lt;/strong&gt;&lt;br&gt;(Create a merge commit)&lt;/td&gt;&lt;td&gt;feature 커밋 전부 + &lt;strong&gt;병합 커밋 1개&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;브랜치가 갈라졌다 합쳐진 모양이 그대로 보임. 이력이 풍부하지만 지저분해질 수 있음&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;strong&gt;Squash and merge&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;PR의 여러 커밋을 &lt;strong&gt;합쳐서 커밋 1개&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;이력이 아주 깔끔(PR=커밋 1개). 대신 개별 커밋과 커밋 컨벤션은 사라짐&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;strong&gt;Rebase and merge&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;feature 커밋들을 &lt;strong&gt;병합 커밋 없이 하나씩 재배치&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;선형(직선) 이력 + 커밋 개수·메시지 그대로 유지. 커밋 컨벤션이 살아있음&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 같은 PR(feature/login: 커밋 3개)을 dev에 합칠 때&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# ① Merge commit&lt;/span&gt;
dev: ...─ Merge pull request #12  &lt;span class=&quot;c&quot;&gt;# 병합 커밋 + feature 3커밋 다 남음&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# ② Squash and merge&lt;/span&gt;
dev: ...─ feat(auth): 로그인 기능 추가   &lt;span class=&quot;c&quot;&gt;# 3커밋 → 1커밋으로 합쳐짐&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# ③ Rebase and merge&lt;/span&gt;
dev: ...─ WIP1 ─ WIP2 ─ 수정3   &lt;span class=&quot;c&quot;&gt;# 병합 커밋 없이 선형으로 그대로 얹힘&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;쉽게 요약&lt;/strong&gt; — &lt;strong&gt;Squash&lt;/strong&gt;는 &quot;PR 하나 = 커밋 하나&quot;로 &lt;strong&gt;합쳐서 깔끔&lt;/strong&gt;하게. &lt;strong&gt;Rebase&lt;/strong&gt;는 병합 커밋을 안 만들고 커밋들을 &lt;strong&gt;직선으로 이어 붙여&lt;/strong&gt; 개별 커밋·메시지를 그대로 살림. &lt;strong&gt;Merge commit&lt;/strong&gt;은 갈래가 합쳐진 흔적(병합 커밋)을 남김.&lt;/div&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;우리 팀 논의 결론&lt;/strong&gt; — Squash는 &lt;strong&gt;커밋 컨벤션이 안 잡힌 팀&lt;/strong&gt;에 특히 유용(어차피 1커밋으로 합치니까)하지만, 그만큼 &lt;strong&gt;커밋 컨벤션·개별 이력이 퇴색&lt;/strong&gt;된다는 의견이 있었다. Claude는 &quot;&lt;strong&gt;커밋 컨벤션이 정해진 팀&lt;/strong&gt;이라면 &lt;strong&gt;Rebase and merge&lt;/strong&gt;&quot;를 추천했다. 우리는 커밋 컨벤션(feat·fix…)을 정해둔 상태라, 잠정 Squash로 모였지만 Rebase 여지도 함께 검토 중이다. (참고: 표준 흐름은 &lt;code&gt;feature → dev&lt;/code&gt; PR, &lt;code&gt;dev&lt;/code&gt;에 PR 없이 반영 금지·CI 필수·리뷰 1명 이상·직접 push 금지)&lt;/div&gt;

    &lt;h2&gt;  주간 미니 프로젝트 — 회의록 시스템&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;기획·발표자료&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;강사님 피드백 반영해 업데이트 후 구글 드라이브 업로드 (완료)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;시연&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;플러그인 사용법·시연 영상 촬영 (완료)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt; ‍  오늘의 코드 리뷰 — 접었다 펴는 UI는 div가 아니라 details/summary다 (HTML 3일차)&lt;/h2&gt;
    &lt;p&gt;브라우저에는 이미 &quot;접었다 펴는&quot; 위젯이 들어 있다. &lt;code&gt;div&lt;/code&gt;로 흉내 내면 JS를 붙여야 하고, 붙여도 키보드·스크린리더는 그냥 빠진다.&lt;/p&gt;

    &lt;h3&gt;❌ 문제 코드 (전체)&lt;/h3&gt;
    &lt;pre&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;html&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;lang&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;charset&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;viewport&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;content&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;width=device-width, initial-scale=1&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;공지&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;공지&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;오늘 수업은 3시에 시작합니다.&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- ▼ 를 손으로 찍어 놓고 접힘은 아직 안 만든 상태 --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;▼ 더보기&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;준비물: 노트북, 충전기. 지각 시 문자 주세요.&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;무엇이 이상했나&lt;/strong&gt; — &quot;▼ 더보기&quot;라고 써 있는데 눌러도 아무 일이 없다. 내용은 처음부터 끝까지 펼쳐진 채고, &lt;code&gt;Tab&lt;/code&gt; 키로 그 자리에 도달하지도 못한다. 여기서 &quot;JS를 아직 안 붙였을 뿐이잖아&quot; 하고 넘어가면 안 된다 — &lt;strong&gt;그 생각 자체가 이 문제의 핵심&lt;/strong&gt;이다. ▼를 손으로 찍고 접힘을 JS로 채우겠다고 마음먹은 순간, 브라우저가 공짜로 주는 태그를 두고 직접 만들기 시작한 것이다.&lt;/div&gt;

    &lt;h3&gt;✅ 정답 (수정 부분)&lt;/h3&gt;
    &lt;pre&gt;  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- 접었다 펴는 건 details/summary 다 --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;t&quot;&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;더보기&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;t&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;준비물: 노트북, 충전기. 지각 시 문자 주세요.&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;왜 details/summary인가&lt;/strong&gt; — &lt;code&gt;div&lt;/code&gt;는 &quot;그냥 상자&quot;라서 ▼는 글자일 뿐이고 접힘 동작은 어디에도 없다. 반면 &lt;code&gt;details/summary&lt;/code&gt;는 브라우저가 세 가지를 공짜로 준다: ① 펼침/접힘 상태(&lt;code&gt;open&lt;/code&gt; 속성이 알아서 켜졌다 꺼짐), ② 키보드 포커스(&lt;code&gt;Tab&lt;/code&gt; 도달) + &lt;code&gt;Enter&lt;/code&gt;/&lt;code&gt;Space&lt;/code&gt;로 열고 닫기, ③ 스크린리더가 &quot;접힌 항목&quot;이라고 읽어줌(role). &lt;code&gt;summary&lt;/code&gt;는 항상 &lt;code&gt;details&lt;/code&gt;의 첫 자식이고 나머지가 접히는 내용이 된다. ▼도 직접 쓰지 않는다 — 브라우저가 삼각형을 그리고 방향까지 뒤집어 준다. 처음부터 펼쳐 두려면 &lt;code&gt;&amp;lt;details open&amp;gt;&lt;/code&gt;. &lt;strong&gt;JS는 한 줄도 필요 없다.&lt;/strong&gt;&lt;/div&gt;

    &lt;h3&gt;  확인 방법&lt;/h3&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;방법&lt;/th&gt;&lt;th&gt;❌ 문제(div)&lt;/th&gt;&lt;th&gt;✅ 정답(details)&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;눌러보고 &lt;code&gt;Tab&lt;/code&gt;으로도 눌러보기&lt;/td&gt;&lt;td&gt;클릭해도 안 접힘, Tab 포커스도 안 감, 내용 계속 보임&lt;/td&gt;&lt;td&gt;처음엔 접힘 → 클릭/Enter/Space로 열고 닫힘&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Console: 키보드로 닿는 요소 세기&lt;br&gt;&lt;code&gt;$$('a[href],button,input,summary,[tabindex]').length&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt; (키보드 사용자에겐 조작할 게 없음)&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt; (summary가 잡힘)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;DevTools → Accessibility → Role&lt;/td&gt;&lt;td&gt;&lt;code&gt;generic&lt;/code&gt; (그냥 글자 상자)&lt;/td&gt;&lt;td&gt;summary=&lt;code&gt;DisclosureTriangle&lt;/code&gt;, details=&lt;code&gt;group&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;실무에서는 이렇게 터진다&lt;/strong&gt; — FAQ·약관·상품 상세·필터 패널 등 접히는 UI는 어디에나 있다. &lt;code&gt;div&lt;/code&gt;+JS로 만들면 JS가 늦게 뜨거나 에러 나는 순간 아무도 못 연다. 접근성 감사(a11y)에 그대로 걸리고, &lt;code&gt;Ctrl+F&lt;/code&gt;로 검색해도 감춰 둔 내용이 안 잡히며, 인쇄하면 통째로 빠진다. &lt;code&gt;details&lt;/code&gt;는 이 전부를 브라우저가 처리한다 — Chrome은 &lt;code&gt;Ctrl+F&lt;/code&gt;로 접힌 안쪽 글자를 찾으면 알아서 펼쳐 준다.&lt;/div&gt;

    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;같이 자주 틀리는 것&lt;/strong&gt;&lt;br&gt;
    ① 여기서 바로 &lt;code&gt;onclick&lt;/code&gt;/JS부터 꺼내는 것(&quot;라이브러리 아코디언 쓰면 되지&quot;도 같은 함정) — 직접 만들면 &lt;code&gt;tabindex=&quot;0&quot;&lt;/code&gt;(도달), &lt;code&gt;keydown&lt;/code&gt;(Enter·Space 둘 다), &lt;code&gt;aria-expanded&lt;/code&gt; 갱신, &lt;code&gt;role&lt;/code&gt; 지정 넷을 다 해야 하고, 대개 Space나 aria-expanded에서 조용히 빠진다. details는 이 넷이 전부 기본값. 순서는 &lt;strong&gt;네이티브 먼저, 안 되는 것만 JS&lt;/strong&gt;.&lt;br&gt;
    ② &lt;code&gt;div&lt;/code&gt;에 &lt;code&gt;tabindex=&quot;0&quot;&lt;/code&gt;만 붙이고 끝 — 포커스는 되지만 Enter/Space 처리·role은 여전히 없다(절반만 고침).&lt;br&gt;
    ③ 기본 삼각형이 디자인과 안 맞는다고 div로 되돌아가는 것 — &lt;code&gt;summary { list-style: none }&lt;/code&gt;으로 지우고 원하는 아이콘을 넣으면 된다.&lt;br&gt;
    ④ 반대로 details를 만능으로 보는 것 — 부드러운 애니메이션, &quot;한 번에 하나만 열리는&quot; 아코디언, 마커 미세 조정이 붙으면 브라우저마다 손이 더 간다. 기본형을 details로 잡아 두고 그때 확장하면 된다.&lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;오늘은 &quot;화면&quot;과 &quot;협업 규칙&quot;을 동시에 다듬은 날이었다. Figma가 아직 손에 안 붙지만, 디자인을 JSON으로 뽑아 Claude로 구현하는 흐름을 알고 나니 툴 익히는 게 덜 막막해졌다. 그리고 머지 정책을 파보면서 &lt;strong&gt;&quot;결과 코드는 같아도 이력의 모양이 달라진다&quot;&lt;/strong&gt;는 걸 처음 제대로 이해했다 — Squash는 깔끔함, Rebase는 커밋 이력 보존. 컨벤션을 정해둔 우리 팀엔 무엇이 맞을지 내일 결론이 난다. 다음 주 8/3부터 본격 구현이라, 지금 뼈대를 잘 잡아두는 게 중요하다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ 포트폴리오 &amp;nbsp;|&amp;nbsp; Figma &amp;nbsp;|&amp;nbsp; 디자인 시스템 &amp;nbsp;|&amp;nbsp; Git 머지 전략 &amp;nbsp;|&amp;nbsp; Squash·Rebase &amp;nbsp;|&amp;nbsp; 협업&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>figma</category>
      <category>Git 머지 전략</category>
      <category>rebase</category>
      <category>squash</category>
      <category>디자인 시스템</category>
      <category>포트폴리오</category>
      <category>학습일지</category>
      <category>협업</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/29</guid>
      <comments>https://yundlab.tistory.com/29#entry29comment</comments>
      <pubDate>Fri, 31 Jul 2026 08:53:57 +0900</pubDate>
    </item>
    <item>
      <title>[260729 22일차] 프로젝트 #3</title>
      <link>https://yundlab.tistory.com/28</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwEy8o/dJMcahk5RWC/BGubMp4edCcdOFL0rsz5y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwEy8o/dJMcahk5RWC/BGubMp4edCcdOFL0rsz5y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwEy8o/dJMcahk5RWC/BGubMp4edCcdOFL0rsz5y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwEy8o%2FdJMcahk5RWC%2FBGubMp4edCcdOFL0rsz5y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;

&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# 포트폴리오 학습일지&lt;/span&gt;
        &lt;h1&gt;[프로젝트 3일차] 컨벤션 병합 · 레포 설정&lt;br&gt;+ 기획서·WBS·요구사항·ERD 작성&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.29 · 오전 팀 회의(협업 규칙 확정) + 개인 산출물(기획 문서 4종)&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;오늘 오전엔 &lt;strong&gt;협업 규칙을 실제로 확정&lt;/strong&gt;했다. 1차로 병합한 &lt;code&gt;CLAUDE.md&lt;/code&gt;를 함께 검토해 코드 컨벤션을 정하고, GitHub 레포를 &lt;strong&gt;백엔드/프론트로 분리&lt;/strong&gt;하기로 했다. 오후엔 개인적으로 &lt;strong&gt;팀 기획서 마무리 + WBS + 요구사항 명세서 + 데이터 ERD&lt;/strong&gt;까지 프로젝트의 뼈대 문서 4종을 작성했다. 회의로 &quot;규칙&quot;을 맞추고, 문서로 &quot;설계&quot;를 잡은 하루였다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;주간 미션&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;회의록 시스템&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;기획서 제출&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;CLAUDE.md&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;1차 병합&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;컨벤션 합의&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;코드 규칙&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;eslint-plugin-import&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;TailwindCSS&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;레포 설정&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;BE/FE 분리&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;기획서&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;노션 템플릿&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;공동 편집&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;내 산출물&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;WBS&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;요구사항&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;ERD&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오전 회의 — 협업 규칙 확정&lt;/h2&gt;
    &lt;p&gt;참석자 전원. 안건은 &lt;strong&gt;주간 미션 공유 · CLAUDE.md 병합 협의 · GitHub 레포 기본 설정 · 프로젝트 기획서&lt;/strong&gt; 네 가지였다.&lt;/p&gt;

    &lt;h3&gt;주간 미션 공유&lt;/h3&gt;
    &lt;p&gt;회의록 시스템 관련 사항을 공유하고, &lt;strong&gt;플러그인 개발과 기획서 제출을 완료&lt;/strong&gt;했다.&lt;/p&gt;

    &lt;h3&gt;CLAUDE.md 병합 협의&lt;/h3&gt;
    &lt;p&gt;각자 만든 규칙을 1차로 병합한 &lt;code&gt;CLAUDE.md&lt;/code&gt;를 함께 확인하고, 아래를 확정했다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;결정&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;import 정렬 도구&lt;/td&gt;&lt;td&gt;&lt;code&gt;eslint-plugin-import&lt;/code&gt; 사용&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;CSS 방법론&lt;/td&gt;&lt;td&gt;&lt;strong&gt;TailwindCSS&lt;/strong&gt; 사용 (shadcn/ui 같은 라이브러리는 미사용)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;백엔드 호스팅&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Render vs AWS 프리티어&lt;/strong&gt; 트레이드오프 확인 후 백엔드 담당이 결정&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h3&gt;GitHub 레포 기본 설정&lt;/h3&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;진행 시점&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;CLAUDE.md 업데이트 이후 레포 기본 설정 진행 (팀장 담당)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;레포 분리&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;백엔드 레포 · 프론트엔드 레포를 분리&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;CLAUDE.md 분리&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;CLAUDE.md도 BE/FE로 나눠 각 레포에 설정&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h3&gt;프로젝트 기획서&lt;/h3&gt;
    &lt;p&gt;월 단위 기획서 제출 기한은 &lt;strong&gt;~7/31&lt;/strong&gt;. 정해진 서식이 없어, &lt;strong&gt;내가 만든 노션 템플릿&lt;/strong&gt;을 팀 공통 서식으로 쓰기로 했고, 팀원 전원이 그 페이지에서 &lt;strong&gt;공동 편집&lt;/strong&gt;으로 작성하기로 했다.&lt;/p&gt;

    &lt;h2&gt;  결정 사항 · 액션 아이템&lt;/h2&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;결정 사항&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;import 정렬 도구 = &lt;code&gt;eslint-plugin-import&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;CSS = TailwindCSS (shadcn/ui 등 미사용)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;백엔드 호스팅 = 담당자가 트레이드오프 확인 후 결정&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;월 단위 기획서 = 내가 만든 노션 템플릿 사용&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;기획서 = 전원 노션 공동 편집으로 작성&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;☐&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;팀원 A(BE)&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;백엔드 호스팅(Render vs AWS) 트레이드오프 확인 후 결정&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;☐&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;전원&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;GitHub 레포 기본 설정(BE/FE 레포 및 CLAUDE.md 분리)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;☐&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;전원&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;월 단위 기획서 노션 템플릿으로 공동 작성·제출 — 기한 2026-07-31&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  내가 한 일 — 기획 문서 4종&lt;/h2&gt;
    &lt;p&gt;오늘은 팀의 설계 뼈대를 세우는 문서들을 집중적으로 작성했다.&lt;/p&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;기획서 마무리&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;우리 팀 프로젝트 기획서를 마무리 (팀 공통 노션 템플릿 기반)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;WBS&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;작업 분해 구조(Work Breakdown Structure) 문서 준비 — 할 일을 단위로 쪼개 일정·담당 정리&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;요구사항 명세서&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;기능/비기능 요구사항 정리&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;ERD&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;데이터 모델링 — 엔티티·관계 설계(ERD) 문서 작성&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;느낀 점&lt;/strong&gt; — 코드보다 문서를 더 많이 쓴 날이었지만, WBS로 &quot;무엇을 언제 누가&quot;를, 요구사항 명세로 &quot;무엇을 만들지&quot;를, ERD로 &quot;데이터를 어떻게 담을지&quot;를 먼저 정리해두니 앞으로의 구현이 훨씬 또렷해졌다. 특히 백엔드를 Claude로 구현할 계획이라, 이 문서들이 자세할수록 결과물 품질이 올라간다는 걸 체감했다.&lt;/div&gt;

    &lt;h2&gt; ‍  오늘의 코드 리뷰 — 폼 값은 name 이 있어야 전송된다 (HTML 2일차)&lt;/h2&gt;

    &lt;p&gt;2일차 코드 리뷰 주제는 &lt;strong&gt;&quot;제출되는 건 &lt;code&gt;name&lt;/code&gt;이 붙은 컨트롤의 값뿐&quot;&lt;/strong&gt;. 로그인 폼에서 아이디·비밀번호를 다 채우고 눌러도 &lt;strong&gt;서버엔 빈 값&lt;/strong&gt;이 도착하는 게 문제였다. 화면상 제출(새로고침)은 멀쩡히 일어나서 더 헷갈린다.&lt;/p&gt;

    &lt;h3&gt;❌ 문제 코드 (전체)&lt;/h3&gt;
    &lt;pre&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;html&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;lang&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;charset&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;viewport&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;content&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;width=device-width, initial-scale=1&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;로그인&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;로그인&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;t&quot;&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;t&quot;&gt;&amp;lt;label&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;for&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;아이디&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;t&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- ❌ name 없음 → 전송 안 됨 --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;t&quot;&gt;&amp;lt;label&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;for&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;비밀번호&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;t&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;password&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- ❌ 여기도 name 없음 --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;t&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;submit&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;로그인&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/pre&gt;

    &lt;h3&gt;✅ 정답 (수정 부분)&lt;/h3&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- 전송이 필요한 입력마다 name 추가 → 서버가 읽는 키가 곧 이 name --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;password&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;

    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;왜 그런가&lt;/strong&gt; — 브라우저는 폼을 제출할 때 &lt;strong&gt;&lt;code&gt;name&lt;/code&gt; 속성이 있는 컨트롤만 직렬화&lt;/strong&gt;한다. &lt;code&gt;id&lt;/code&gt;만 있고 &lt;code&gt;name&lt;/code&gt;이 없으면 그 값은 제출 데이터에 안 담긴다. &lt;code&gt;id&lt;/code&gt;는 &lt;strong&gt;label 연결(for)·CSS·JS 선택자용&lt;/strong&gt;일 뿐 전송과는 무관. 그래서 &quot;입력은 했는데 서버엔 안 온다&quot;가 된다.&lt;/div&gt;

    &lt;h3&gt;확인 방법&lt;/h3&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;방법&lt;/th&gt;&lt;th&gt;문제(name 없음)&lt;/th&gt;&lt;th&gt;정답(name 있음)&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;채우고 로그인 → 주소창(URL)&lt;/td&gt;&lt;td&gt;&lt;code&gt;...?&lt;/code&gt; 쿼리 비어 있음&lt;/td&gt;&lt;td&gt;&lt;code&gt;...?userId=...&amp;amp;userPw=...&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Console &lt;code&gt;[...new FormData(document.querySelector('form'))]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;[]&lt;/code&gt; (빈 배열)&lt;/td&gt;&lt;td&gt;&lt;code&gt;[['userId','...'],['userPw','...']]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;DevTools Elements&lt;/td&gt;&lt;td&gt;input에 &lt;code&gt;id&lt;/code&gt;만&lt;/td&gt;&lt;td&gt;&lt;code&gt;name=&quot;userId&quot;&lt;/code&gt;·&lt;code&gt;name=&quot;userPw&quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;code&gt;FormData&lt;/code&gt;는 &lt;strong&gt;name이 있는 컨트롤만&lt;/strong&gt; 담으므로, 값이 안 실리는 직접 증거가 된다. (file://로 열면 쿼리가 안 보일 수 있어 Live Server 권장 — 콘솔 확인은 어느 쪽에서도 확실)&lt;/div&gt;

    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;실무에선 이렇게 터진다&lt;/strong&gt; — 백엔드에서 &lt;code&gt;req.query.userId&lt;/code&gt;/&lt;code&gt;req.body.userId&lt;/code&gt;가 계속 &lt;code&gt;undefined&lt;/code&gt;. 프론트는 &quot;분명히 입력했는데&quot; 서버는 빈 값이라 원인을 한참 못 찾는다. &lt;strong&gt;name 오타&lt;/strong&gt;(&lt;code&gt;user_id&lt;/code&gt;↔&lt;code&gt;userId&lt;/code&gt;)는 더 악질 — 렌더·제출은 되는데 서버가 기대하는 키와만 안 맞아 &lt;strong&gt;조용히, 에러 없이&lt;/strong&gt; 실패한다.&lt;/div&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;같이 자주 틀리는 것&lt;/strong&gt; — ① &lt;code&gt;id&lt;/code&gt;로 값이 전송된다고 착각(전송은 오직 &lt;code&gt;name&lt;/code&gt;), ② name을 서버 기대 키와 다르게(오타·대소문자·스네이크/카멜 혼용), ③ 로그인 폼을 &lt;code&gt;GET&lt;/code&gt;(기본값) 그대로 두기 → 비밀번호가 URL·기록에 남으므로 실무는 &lt;code&gt;method=&quot;post&quot;&lt;/code&gt;, ④ &lt;code&gt;checkbox&lt;/code&gt;·&lt;code&gt;radio&lt;/code&gt;는 name이 없으면 값은커녕 그룹조차 안 묶인다.&lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;오늘은 &quot;손으로 코드를 짜는&quot; 날이 아니라 &lt;strong&gt;&quot;프로젝트의 뼈대를 세우는&quot;&lt;/strong&gt; 날이었다. 컨벤션(eslint·Tailwind)과 레포 구조(BE/FE 분리)를 팀이 합의하고, 나는 기획서·WBS·요구사항·ERD로 설계의 밑그림을 그렸다. 규칙과 설계를 먼저 맞춰두면 나중에 각자 병렬로 달릴 때 충돌이 줄어든다는 걸 다시 느꼈다. 내일부터는 이 문서들을 기준으로 실제 구현에 들어간다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ 포트폴리오 &amp;nbsp;|&amp;nbsp; 팀 프로젝트 &amp;nbsp;|&amp;nbsp; CLAUDE.md &amp;nbsp;|&amp;nbsp; WBS &amp;nbsp;|&amp;nbsp; 요구사항 명세 &amp;nbsp;|&amp;nbsp; ERD&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>CLAUDE.md</category>
      <category>ERD</category>
      <category>wes</category>
      <category>요구사항 명세</category>
      <category>팀 프로젝트</category>
      <category>포트폴리오</category>
      <category>학습일지</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/28</guid>
      <comments>https://yundlab.tistory.com/28#entry28comment</comments>
      <pubDate>Thu, 30 Jul 2026 09:09:04 +0900</pubDate>
    </item>
    <item>
      <title>[260728 21일차] 프로젝트 #2</title>
      <link>https://yundlab.tistory.com/27</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI52kg/dJMcadv1jja/VeaOv1dZMmDfiERW9xknq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI52kg/dJMcadv1jja/VeaOv1dZMmDfiERW9xknq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI52kg/dJMcadv1jja/VeaOv1dZMmDfiERW9xknq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI52kg%2FdJMcadv1jja%2FVeaOv1dZMmDfiERW9xknq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;

&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# 포트폴리오 학습일지&lt;/span&gt;
        &lt;h1&gt;[프로젝트 2일차] 주제 선정 · 회의록 시스템 미션&lt;br&gt;Git 전략 · 하네스 엔지니어링&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.28 · 오전·오후 팀 회의 (주제 확정 → 협업 규칙·운영 방식 결정)&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;오늘은 &lt;strong&gt;프로젝트 주제를 확정&lt;/strong&gt;하고, 그걸 어떻게 굴릴지 &lt;strong&gt;운영 방식까지 못 박은&lt;/strong&gt; 날이었다. 오전엔 팀원 각자가 준비해 온 기획을 소개·투표해 주제를 골랐고, 오후엔 확정된 주제와 함께 &lt;strong&gt;주간 미션(회의록 시스템)&lt;/strong&gt;, &lt;strong&gt;Git 브랜치 전략&lt;/strong&gt;, 그리고 프로젝트 운영에 &lt;strong&gt;하네스 엔지니어링&lt;/strong&gt;을 도입하기로 했다. &quot;무엇을 만들지&quot;에서 &quot;어떻게 같이 만들지&quot;로 넘어가는 단계였다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;주제 선정&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;기획 4개&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;투표&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;확정 주제&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;실시간 피드백 모니터링&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;주간 미션&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;회의록 시스템&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;플러그인&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;Notion MCP&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;Git 전략&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;main/dev&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;feature/**&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;PR 보호&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;운영&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;하네스 엔지니어링&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;기타&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;API 계약서&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;issue-log 연동&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오전 회의 — 주제 선정&lt;/h2&gt;

    &lt;p&gt;안건은 &lt;strong&gt;&quot;주제 선정을 위한 팀원별 기획 소개&quot;&lt;/strong&gt;. 각자 준비한 기획을 발표하고, 서로의 기획을 상세히 검토한 뒤 투표하기로 했다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;제안자&lt;/th&gt;&lt;th&gt;기획 주제&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;팀원 A&lt;/td&gt;&lt;td&gt;실시간 이벤트 피드백 모니터링 서비스&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;팀원 B&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;AI 기반 감정 일기장 서비스&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h3&gt;오전 액션 아이템&lt;/h3&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;검토·투표&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;오전 11:30까지 다른 팀원 기획을 상세 검토 후 투표&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;Claude 검토&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;전체 기획 문서를 Claude에 전달 → &quot;4주·4명&quot; 팀 상황에 맞게 구현 가능한 주제인지 검토 요청&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;API 계약서&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;주제 선정 후, 생성형 모델 API 호출 계약서용 범주 설정&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;API 계약서 범주&lt;/strong&gt; — 생성형 모델을 쓴다면 &lt;strong&gt;사용처 · 예상 월 금액 · 과금 책임 여부&lt;/strong&gt;를 정해 계약서(제약서)에 담아야 한다. 주제를 고를 때 &quot;AI를 어디에·얼마나 쓰는가&quot;를 함께 따져야 한다는 점이 인상적이었다.&lt;/div&gt;

    &lt;h2&gt;✅ 오후 회의 — 주제 확정&lt;/h2&gt;

    &lt;p&gt;참석자 전원. 오전 검토·투표 결과, 주제는 &lt;strong&gt;「실시간 피드백 모니터링 서비스」&lt;/strong&gt;(팀원 A 제안)로 확정됐다.&lt;/p&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;주제 확정 — 실시간 피드백 모니터링 서비스&lt;/strong&gt;. 4주·4명이라는 팀 상황과 구현 가능성을 함께 고려한 결과였다.&lt;/div&gt;

    &lt;h2&gt;  주간 미션 — 회의록 시스템&lt;/h2&gt;

    &lt;p&gt;이번 주 미션은 &lt;strong&gt;&quot;회의록 시스템&quot;&lt;/strong&gt;. raw 회의록 마크다운을 넣으면 정해진 서식으로 정리해 주는 도구를 직접 만든다.&lt;/p&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;정리&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;raw 회의록 .md를 전달하면 정해진 서식에 맞게 섹션 분리·정리&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;자동 작성&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Notion MCP 연결로 팀스페이스 '회의록' 섹션에 페이지 자동 생성&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;액션 아이템&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;회의록의 액션 아이템을 '액션 아이템' 섹션에 자동 추가&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;형식&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Claude Code에서 쓰는 플러그인 방식&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;왜 플러그인 방식인가&lt;/strong&gt; — ① &lt;strong&gt;도구 최소화&lt;/strong&gt;(Notion 팀스페이스로 관리 플랫폼 통합), ② &lt;strong&gt;배포·사용 편의&lt;/strong&gt;(GitHub repo로 배포, 몇 줄 명령어로 설치). 며칠 전 배운 플러그인·MCP가 바로 팀 도구로 이어졌다.&lt;/div&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;기한&lt;/strong&gt; — 팀장이 &lt;strong&gt;7/29(수) 15:00까지 회의록 플러그인 기획서&lt;/strong&gt;를 작성·제출한다.&lt;/div&gt;

    &lt;h2&gt;  Github Repo &amp; 브랜치 전략&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;직접 권한&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Organization Repo에서 fork 없이, 각 멤버에게 쓰기 권한 부여 → 같은 repo에 직접 접근&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;브랜치 보호&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;main·dev에 바로 병합 못 하게 보호 장치 필요 (Claude로 방안 확인·검토 예정)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;브랜치 전략&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;main·dev 기본 + feature/**·refactor/** 를 만들어 PR/커밋&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;issue-log&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;issue-log 플러그인과의 연동 고려&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 브랜치 전략&lt;/span&gt;
main      &lt;span class=&quot;c&quot;&gt;# 안정 버전&lt;/span&gt;
 └ dev    &lt;span class=&quot;c&quot;&gt;# 통합 개발&lt;/span&gt;
    ├ feature/**    &lt;span class=&quot;c&quot;&gt;# 기능 개발&lt;/span&gt;
    └ refactor/**   &lt;span class=&quot;c&quot;&gt;# 리팩토링&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# main·dev 직접 병합 금지 → PR로만&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;PR 메시지에 하네스 엔지니어링 적용&lt;/strong&gt; — Claude와 함께 구현한 코드 중 &lt;strong&gt;&quot;이해가 잘 안 된 부분&quot;도 PR에 명시&lt;/strong&gt;하기로 했다. AI가 짠 코드를 그냥 넘기지 않고, 모르는 곳을 드러내 팀이 함께 검토하는 안전장치 — 하네스의 감각을 협업 규칙으로 옮긴 셈이다.&lt;/div&gt;

    &lt;h2&gt;  환경 설정 — 하네스 엔지니어링 도입&lt;/h2&gt;
    &lt;p&gt;프로젝트 운영 자체에 &lt;strong&gt;하네스 엔지니어링&lt;/strong&gt;을 도입하기로 했다. 각자 &lt;strong&gt;하네스에 추가할 환경설정 항목&lt;/strong&gt;을 검색·조사해 오고, 오후 15:30에 다시 모여 어떤 항목을 넣을지 결정한다.&lt;/p&gt;

    &lt;h2&gt;  결정 사항 · 액션 아이템&lt;/h2&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;결정 사항&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;주제: &lt;strong&gt;실시간 피드백 모니터링 서비스&lt;/strong&gt; 선정&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Repo: Organization Repo에서 &lt;strong&gt;fork 없이 멤버별 쓰기 권한&lt;/strong&gt; 부여&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;브랜치 전략: &lt;code&gt;main&lt;/code&gt;·&lt;code&gt;dev&lt;/code&gt; + &lt;code&gt;feature/**&lt;/code&gt;·&lt;code&gt;refactor/**&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;PR 메시지에 &lt;strong&gt;이해 안 되는 부분 명시&lt;/strong&gt;하는 하네스 엔지니어링 적용&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;프로젝트 운영에 &lt;strong&gt;하네스 엔지니어링 도입&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;☐&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;팀장&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;회의록 플러그인 기획서 작성·제출 — 기한 2026-07-29&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;☐&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;전원&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;main·dev 병합 보호 장치 방안 확인·검토 (Claude 활용)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;☐&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;전원&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;하네스 환경설정 항목 검색·조사 — 기한 2026-07-28&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt; ‍  오늘의 코드 리뷰 — label vs p (HTML 1일차)&lt;/h2&gt;

    &lt;p&gt;7/28부터 &lt;strong&gt;매일 코드 리뷰&lt;/strong&gt;가 시작됐다. 첫 주제는 &lt;strong&gt;&quot;입력칸의 이름표는 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;가 아니라 &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;로 붙인다&quot;&lt;/strong&gt;. 겉으론 멀쩡한 회원가입 폼인데, &quot;아이디&quot; 글자를 눌러도 입력칸에 커서가 안 들어가는 게 문제였다.&lt;/p&gt;

    &lt;h3&gt;❌ 문제 코드 (전체)&lt;/h3&gt;
    &lt;pre&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;html&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;lang&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;charset&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;viewport&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;content&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;width=device-width, initial-scale=1&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;회원가입&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;회원가입&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;t&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;action&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;/signup&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;method&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;post&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;&amp;lt;p&amp;gt;아이디&amp;lt;/p&amp;gt;              &amp;lt;!-- ❌ 이름표를 p 로 --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;t&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt; required&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;c&quot;&gt;&amp;lt;p&amp;gt;비밀번호&amp;lt;/p&amp;gt;            &amp;lt;!-- ❌ 여기도 p --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;t&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;password&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt; required&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;t&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;submit&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;가입하기&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;t&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/pre&gt;

    &lt;h3&gt;✅ 정답 (수정 부분)&lt;/h3&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- p → label 로, for 의 값은 input 의 id 와 같게 --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;label&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;for&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;아이디&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userId&quot;&lt;/span&gt; required&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;

&lt;span class=&quot;t&quot;&gt;&amp;lt;label&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;for&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;tx&quot;&gt;비밀번호&lt;/span&gt;&lt;span class=&quot;t&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class=&quot;t&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;password&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;v&quot;&gt;&quot;userPw&quot;&lt;/span&gt; required&lt;span class=&quot;t&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- for 의 값 = input 의 id (한 글자만 달라도 안 묶임) --&amp;gt;&lt;/span&gt;&lt;/pre&gt;

    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;왜 label이어야 하나&lt;/strong&gt; — &lt;code&gt;label&lt;/code&gt;에 &lt;code&gt;for&lt;/code&gt;를 쓰면 브라우저가 그 입력칸과 한 쌍으로 묶어준다. 그래서 ① &lt;strong&gt;글자를 클릭하면 입력칸에 커서&lt;/strong&gt;가 들어가고, ② &lt;strong&gt;스크린리더가 &quot;아이디, 입력&quot;&lt;/strong&gt;처럼 무슨 칸인지 읽어준다. &lt;code&gt;for&lt;/code&gt;에 적는 값이 곧 입력칸의 &lt;code&gt;id&lt;/code&gt;라, id가 필요했던 것.&lt;/div&gt;

    &lt;h3&gt;확인 방법&lt;/h3&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;방법&lt;/th&gt;&lt;th&gt;문제(p)&lt;/th&gt;&lt;th&gt;정답(label)&lt;/th&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;Console &lt;code&gt;document.querySelector('#userId').labels.length&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt; (연결된 이름표 없음)&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt; (이름표와 묶임)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;F12 → Accessibility → Name&lt;/td&gt;&lt;td&gt;비어 있음&lt;/td&gt;&lt;td&gt;&quot;아이디&quot;&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;실무에선 이렇게 터진다&lt;/strong&gt; — 화면이 멀쩡해 보여 끝까지 발견 안 됨. 모바일에선 label이 묶여 있어야 글자 영역까지 눌러 커서를 넣을 수 있어 &lt;strong&gt;오탭이 준다&lt;/strong&gt;. 스크린리더 사용자에겐 &lt;strong&gt;이름 없는 칸&lt;/strong&gt;이 되어 가입 자체가 막힌다. 입력칸을 만들 때마다 label을 같이 쓰는 습관이 필요.&lt;/div&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;같이 자주 틀리는 것&lt;/strong&gt; — ① &lt;code&gt;placeholder&lt;/code&gt;로 이름표를 대신하기(입력하면 사라져 무슨 칸인지 모름 → label 대체 불가), ② &lt;code&gt;for&lt;/code&gt;에 &lt;code&gt;name&lt;/code&gt; 값 적기(&lt;code&gt;for&lt;/code&gt;는 &lt;strong&gt;id&lt;/strong&gt;를 가리킨다, name 아님).&lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;주제를 정하는 것보다 인상 깊었던 건, &quot;AI로 짠 코드를 어떻게 우리 것으로 만드느냐&quot;를 규칙으로 못 박은 부분이었다. PR에 &lt;strong&gt;이해 안 된 부분을 일부러 적는&lt;/strong&gt; 하네스 방식은, 며칠 전 배운 개념이 실제 팀 워크플로로 살아나는 순간이라 반가웠다. 회의록 시스템 미션도 결국 우리가 배운 플러그인·MCP를 팀 도구로 만드는 일이라, 배운 게 곧바로 쓰인다는 게 좋았다. 내일은 각자 하네스 항목을 들고 와 운영 규칙을 확정한다 — 판이 점점 갖춰지고 있다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ 포트폴리오 &amp;nbsp;|&amp;nbsp; 팀 프로젝트 &amp;nbsp;|&amp;nbsp; 주제선정 &amp;nbsp;|&amp;nbsp; Git 전략 &amp;nbsp;|&amp;nbsp; 하네스 엔지니어링 &amp;nbsp;|&amp;nbsp; 회의록&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>Git 전략</category>
      <category>주제선정</category>
      <category>팀 프로젝트</category>
      <category>포트폴리오</category>
      <category>하네스 엔지니어링</category>
      <category>학습일지</category>
      <category>회의록</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/27</guid>
      <comments>https://yundlab.tistory.com/27#entry27comment</comments>
      <pubDate>Tue, 28 Jul 2026 16:38:01 +0900</pubDate>
    </item>
    <item>
      <title>[260727 20일차] 프로젝트 #1</title>
      <link>https://yundlab.tistory.com/26</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bClI0y/dJMcaixtncu/taPKzm51TILgbqam9nvOk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bClI0y/dJMcaixtncu/taPKzm51TILgbqam9nvOk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bClI0y/dJMcaixtncu/taPKzm51TILgbqam9nvOk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbClI0y%2FdJMcaixtncu%2FtaPKzm51TILgbqam9nvOk0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# 포트폴리오 학습일지&lt;/span&gt;
        &lt;h1&gt;[킥오프] 팀 프로젝트 시작&lt;br&gt;조 편성 · 협업 컨벤션 · 팀 멘토링&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.27 · 학습 범위: 포트폴리오 프로젝트 킥오프 (팀 세팅·Git 규칙·멘토링)&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;오늘부터 &lt;strong&gt;포트폴리오 팀 프로젝트&lt;/strong&gt;가 시작됐다. 코드를 배우는 날이라기보다, &lt;strong&gt;같이 일할 판을 짜는 날&lt;/strong&gt;이었다. 팀을 꾸리고, 소통·갈등 조정 방식을 정하고, Git 브랜치·커밋·코드 스타일 같은 &lt;strong&gt;협업 컨벤션&lt;/strong&gt;을 합의했다. 오후엔 팀 멘토링으로 Github 관리·API 비용·코드 리뷰·주간 미션 범위 같은 실무 규칙도 확인했다. 혼자 코딩하던 것과 달리 &quot;규칙부터 맞추고 시작한다&quot;는 감각이 새로웠다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;팀 구성&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;팀장 선정&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;Slack&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;Git 브랜치&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;main→dev→feature&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;PR 병합&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;커밋&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Conventional&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;feat·fix·docs&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;코드 스타일&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Prettier&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;ESLint&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;멘토링&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;레포 관리&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;API 제약서&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;다음 할 일&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;기획서&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;28일 투표&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  팀 세팅&lt;/h2&gt;

    &lt;p&gt;먼저 팀을 꾸렸다. 팀명·로고는 무겁게 가지 않고 후보만 가볍게 정한 뒤 확정은 뒤로 미뤘고(임시), &lt;strong&gt;팀장도 정했다&lt;/strong&gt;(실명은 생략). 소통은 &lt;strong&gt;Slack 채널&lt;/strong&gt;로 하기로 했다.&lt;/p&gt;

    &lt;div class=&quot;callout blue&quot;&gt;
        &lt;strong&gt;갈등 조정 규칙 (개발자스러운 재치)&lt;/strong&gt; — 의사결정 기준을 삼항 연산자로 정리했다:
        &lt;pre style=&quot;margin-top:10px;&quot;&gt;isCritical &lt;span class=&quot;k&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;v&quot;&gt;&quot;클로드 포함 5인 다수결&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;v&quot;&gt;&quot;사소한 건 팀장 자율&quot;&lt;/span&gt;&lt;/pre&gt;
        중요한(critical) 사안은 &lt;strong&gt;Claude까지 포함해 5인 다수결&lt;/strong&gt;, 사소한 건 팀장이 빠르게 판단. &quot;무엇이 중요한 결정인가&quot;를 먼저 나눈 게 핵심이었다.
    &lt;/div&gt;

    &lt;h2&gt;  Git &amp; 협업 컨벤션&lt;/h2&gt;

    &lt;h3&gt;브랜치 전략&lt;/h3&gt;
    &lt;p&gt;&lt;code&gt;main → dev → feature&lt;/code&gt; 3단 브랜치로 가기로 했다. 기능은 &lt;code&gt;feature&lt;/code&gt;에서 만들어 &lt;code&gt;dev&lt;/code&gt;로 모으고, 안정된 것만 &lt;code&gt;main&lt;/code&gt;으로 올린다. (모노레포 여부는 기획이 확정된 뒤 결정)&lt;/p&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;
        &lt;strong&gt;메인 브랜치 직접 푸시 금지&lt;/strong&gt; — &lt;code&gt;main&lt;/code&gt;에는 직접 push하지 않고 &lt;strong&gt;PR(Pull Request)을 통해서만 병합&lt;/strong&gt;한다. 실수 방지를 위해 &lt;strong&gt;hook으로 한 번 더 체크&lt;/strong&gt;하기로 했다(며칠 전 배운 커스텀 훅이 실제 협업 규칙으로 이어진 셈).
    &lt;/div&gt;

    &lt;h3&gt;커밋 컨벤션 (Conventional Commits)&lt;/h3&gt;
    &lt;p&gt;커밋 메시지는 &lt;strong&gt;컨벤셔널 커밋&lt;/strong&gt; 형식을 따르고, 타입 태그를 붙인다. 커밋 본문 내용 형식까지는 아직 강제하지 않기로 했다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;타입&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;feat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;새로운 기능 추가&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;fix&lt;/code&gt;&lt;/td&gt;&lt;td&gt;버그 수정&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;docs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;문서 수정 (코드 변경 없음)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;style&lt;/code&gt;&lt;/td&gt;&lt;td&gt;포맷팅·세미콜론 등 스타일 변경 (논리 변경 없음)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;refactor&lt;/code&gt;&lt;/td&gt;&lt;td&gt;리팩토링 (기능 변화 없음)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;test&lt;/code&gt;&lt;/td&gt;&lt;td&gt;테스트 코드 추가/수정&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;chore&lt;/code&gt;&lt;/td&gt;&lt;td&gt;빌드·패키지 설정 등 기타 작업&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;design&lt;/code&gt;&lt;/td&gt;&lt;td&gt;CSS 등 UI 디자인 변경&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;comment&lt;/code&gt;&lt;/td&gt;&lt;td&gt;필요한 주석 추가/변경&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h3&gt;코드 스타일&lt;/h3&gt;
    &lt;p&gt;포매터·린터로 스타일을 통일해 &quot;누가 짜도 같은 모양&quot;이 되게 맞췄다.&lt;/p&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;Prettier&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;코드 자동 포매팅&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;ESLint&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;문법·규칙 검사&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;세미콜론&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;문장 끝 세미콜론 사용&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;싱글 쿼트&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;문자열은 작은따옴표&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;들여쓰기&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;4칸(스페이스)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  팀 컨벤션 (소통)&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;공개 소통&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;1:1 DM 지양, Slack 채널에서 소통 — 모두가 진행상황·피드백 공유&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;확인 표시&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;공지·게시물 확인 후 체크 이모지 남기기&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;API 비용 관련 방향&lt;/strong&gt; — 운영 측에 문의한 결과 추가 비용 지원이 현실적으로 어렵다는 답을 받아, 일단 &lt;strong&gt;생성형 모델 API 호출이 없는 기획&lt;/strong&gt; 쪽으로 방향을 잡는 것을 고려하기로 했다.&lt;/div&gt;

    &lt;h2&gt; ‍  팀 멘토링 정리 (2026-07-27)&lt;/h2&gt;

    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;주제&lt;/th&gt;&lt;th&gt;내용&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Github 관리&lt;/td&gt;&lt;td&gt;별도 HANCOM organization 없음. &lt;strong&gt;팀장이 레포 생성 → 팀원을 collaborator로 초대&lt;/strong&gt;. 강사님이 각 팀 레포 주소를 수집. 레포 관리 방식은 기획 완료 후 협의.&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Claude API 비용 지원&lt;/td&gt;&lt;td&gt;&lt;strong&gt;&quot;제약서&quot; 작성&lt;/strong&gt; 필요 — 기획에서 &quot;API를 어디에·얼마나 활용할지&quot;를 알려야 함. 정확한 서식은 미전달, 일단 제출 후 피드백받기.&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;코드 리뷰&lt;/td&gt;&lt;td&gt;(주의) &lt;strong&gt;팀 프로젝트 코드를 리뷰하는 게 아님!&lt;/strong&gt; 강의에서 배운 내용(예: JS의 class)에 대한 코드를 리뷰하는 방식.&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;주간 미션 &quot;회의록 시스템 개발&quot;&lt;/td&gt;&lt;td&gt;개발에 도움이 되는 &lt;strong&gt;도구를 직접 만들어보는 경험&lt;/strong&gt;이 목적. 웹 서비스가 아니어도 되고, 플러그인 등 형태는 팀 내 결정.&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Notion&lt;/td&gt;&lt;td&gt;초대된 HANCOM 팀스페이스의 &lt;strong&gt;&quot;E조_한컴&quot;&lt;/strong&gt; 문서 사용.&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;기획 규모&lt;/td&gt;&lt;td&gt;기획에 시간·노력이 많이 듦. &quot;기획→구현&quot; 반복은 선호되지 않음 → 결국 &lt;strong&gt;팀 내 결정이 최선&lt;/strong&gt;.&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;참고 — 팀원이 만든 이슈 로그 플러그인&lt;/strong&gt; — 프로젝트 진행하며 매일 이슈를 기록하려고, &lt;strong&gt;Notion MCP&lt;/strong&gt;로 현재 세션의 이슈를 정해진 형식으로 개인 노션 페이지에 써주는 플러그인을 만든 팀원이 있었다(&lt;a href=&quot;https://github.com/kimnioyh/issue-log&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github.com/kimnioyh/issue-log&lt;/a&gt;). 며칠 전 배운 플러그인·MCP가 바로 팀 협업 도구로 이어지는 게 인상적이었다.&lt;/div&gt;

    &lt;h2&gt;  팀장 멘토링 — 일정 · 서버&lt;/h2&gt;

    &lt;p&gt;팀장 멘토링에서 공유된 &lt;strong&gt;제출 일정과 서버 방식&lt;/strong&gt;을 정리했다. 주 단위(회의록 시스템)와 월 단위(미정 프로젝트) 기획안 마감이 각각 잡혔다.&lt;/p&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;  서버가 필요할 때&lt;/strong&gt; — 남는 PC에 &lt;strong&gt;리눅스 OS를 설치&lt;/strong&gt;하거나 &lt;strong&gt;AWS 프리티어&lt;/strong&gt; 등을 사용한다. 정답은 없고 &lt;strong&gt;팀에 맞는 방식&lt;/strong&gt;을 고르면 된다.&lt;/div&gt;

    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;구분&lt;/th&gt;&lt;th&gt;마감 · 내용&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;주 단위 — 회의록 시스템&lt;/td&gt;&lt;td&gt;&lt;strong&gt;7/29(수)&lt;/strong&gt;까지 기획안 제출 · Notion 팀 페이지 &lt;strong&gt;&quot;미니 프로젝트&quot;&lt;/strong&gt; 섹션에 작성&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;월 단위 — 미정 프로젝트&lt;/td&gt;&lt;td&gt;&lt;strong&gt;7/31(금)&lt;/strong&gt;까지 기획안 제출&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;중간 발표&lt;/td&gt;&lt;td&gt;약 &lt;strong&gt;3주 뒤&lt;/strong&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;/table&gt;

    &lt;h2&gt;✅ 다음 할 일&lt;/h2&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;기획서&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;각자 프로젝트 주제 및 기획서 작성해오기 (중요)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;주제 투표&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;28일(화) — Claude 포함 총 5명 투표로 주제 결정&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;그동안은 &quot;무엇을 어떻게 만드느냐&quot;를 배웠다면, 오늘은 &lt;strong&gt;&quot;여럿이 어떻게 같이 만드느냐&quot;&lt;/strong&gt;를 정하는 날이었다. 브랜치 전략·커밋 컨벤션·코드 스타일처럼, 혼자면 안 정해도 되는 것들을 미리 합의해두는 게 결국 나중의 충돌을 줄인다는 걸 느꼈다. 특히 &lt;code&gt;main&lt;/code&gt; 직접 푸시를 훅으로 막고 PR로만 병합하기로 한 부분은, 얼마 전 배운 커스텀 훅이 실제 협업 규칙으로 살아나는 순간이라 반가웠다. 내일은 각자 기획서를 들고 와 주제를 정한다 — 이제 진짜 &quot;우리 것&quot;을 만들기 시작한다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ 포트폴리오 &amp;nbsp;|&amp;nbsp; 팀 프로젝트 &amp;nbsp;|&amp;nbsp; Git 컨벤션 &amp;nbsp;|&amp;nbsp; 협업 &amp;nbsp;|&amp;nbsp; 킥오프 &amp;nbsp;|&amp;nbsp; 학습일지&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>Git 컨벤션</category>
      <category>킥오프</category>
      <category>팀 프로젝트</category>
      <category>포트폴리오</category>
      <category>학습일지</category>
      <category>협업</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/26</guid>
      <comments>https://yundlab.tistory.com/26#entry26comment</comments>
      <pubDate>Tue, 28 Jul 2026 11:02:15 +0900</pubDate>
    </item>
    <item>
      <title>[260724 19일차] Claude Code 심화</title>
      <link>https://yundlab.tistory.com/25</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSzPuB/dJMb99UMhFZ/Q0PCJbiZB16CGwfOov0KKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSzPuB/dJMb99UMhFZ/Q0PCJbiZB16CGwfOov0KKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSzPuB/dJMb99UMhFZ/Q0PCJbiZB16CGwfOov0KKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSzPuB%2FdJMb99UMhFZ%2FQ0PCJbiZB16CGwfOov0KKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;

&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# AI 도구 · 자동화 학습일지&lt;/span&gt;
        &lt;h1&gt;[Claude Code 심화]&lt;br&gt;커스텀 훅 · 플러그인 · MCP 자동화&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.24 · 학습 범위: Claude Code 도구·명령어(advisor·caveman·skills.sh 등), 커스텀 훅(PreToolUse), 플러그인 제작, MCP&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;오늘은 &lt;strong&gt;&quot;Claude Code를 내 손에 맞게 길들이는&quot;&lt;/strong&gt; 하루였다. 세션을 관리하는 CLI 명령어부터, 위험한 명령을 자동으로 가로채는 &lt;strong&gt;커스텀 훅&lt;/strong&gt;, 스킬·훅·에이전트·MCP를 한데 묶은 &lt;strong&gt;플러그인 제작&lt;/strong&gt;, 그리고 노션 MCP를 연결해 이 학습일지를 자동으로 쓰는 &lt;strong&gt;자동화&lt;/strong&gt;까지 직접 만들어봤다. 어제까지 도구를 &quot;쓰는&quot; 쪽이었다면, 오늘은 도구를 &quot;만드는&quot; 쪽으로 넘어간 느낌이었다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;CLI 명령어&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;/rename&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;/resume&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;@agent&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;커스텀 훅&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;PreToolUse&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;stdin JSON&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;exit code&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;안전 가드&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;rm -rf&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;git reset&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;DROP&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;플러그인&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;스킬&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;훅&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;에이전트&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;MCP&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;배포&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;마켓플레이스&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;자동화&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;notion MCP&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;/daily-log&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  Claude Code CLI — 세션 다루기&lt;/h2&gt;

    &lt;p&gt;세션을 오래 쓰다 보면 대화가 여러 개로 쌓인다. 오늘은 그 세션을 &lt;strong&gt;이름 붙이고 다시 찾아 들어가는&lt;/strong&gt; 명령어를 익혔다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;명령어&lt;/th&gt;&lt;th&gt;역할&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/rename&lt;/code&gt;&lt;/td&gt;&lt;td&gt;현재 세션에 제목을 붙여 나중에 알아보기 쉽게 함&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/resume&lt;/code&gt;&lt;/td&gt;&lt;td&gt;이전 세션 목록을 모두 보고 원하는 세션으로 찾아 들어감&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;code&gt;/resume&lt;/code&gt;은 &lt;strong&gt;&quot;어제 하던 그 작업&quot;&lt;/strong&gt;으로 정확히 되돌아갈 때 유용하다. 세션마다 &lt;code&gt;/rename&lt;/code&gt;으로 제목을 붙여두면, 나중에 목록에서 헤매지 않고 바로 골라 이어갈 수 있다.&lt;/div&gt;

    &lt;h3&gt;@에이전트로 홈페이지 만들기&lt;/h3&gt;
    &lt;p&gt;&lt;code&gt;@에이전트이름&lt;/code&gt;으로 특정 서브에이전트를 호출해 작업을 통째로 맡길 수 있다. 오늘은 &lt;code&gt;@&quot;늙크크&quot;&lt;/code&gt; 에이전트를 불러 홈페이지(&lt;code&gt;index.html&lt;/code&gt;) 한 벌을 만들어봤다. 프롬프트로 일일이 지시하는 대신, 목적에 맞게 설계된 에이전트에게 &quot;만들어줘&quot; 한 번으로 결과물이 나온다는 게 편했다.&lt;/p&gt;

    &lt;h2&gt;  Claude Code 도구·명령어 모음 (허브 카드)&lt;/h2&gt;

    &lt;p&gt;Claude 학습 허브에는 Claude Code를 확장하는 &lt;strong&gt;슬래시 명령어·도구 카드&lt;/strong&gt;가 잔뜩 있었다. 오늘 핵심 실습(훅·플러그인·MCP)과 함께 이 도구들도 한 바퀴 훑었다.&lt;/p&gt;

    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;명령어 / 도구&lt;/th&gt;&lt;th&gt;역할&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/advisor&lt;/code&gt;&lt;/td&gt;&lt;td&gt;더 강한 모델을 &lt;strong&gt;조언자&lt;/strong&gt;로 붙여 중요한 순간에 한 번 더 검토 (실험적·API 전용, 토큰 별도 과금)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/init&lt;/code&gt;&lt;/td&gt;&lt;td&gt;코드베이스를 분석해 &lt;code&gt;CLAUDE.md&lt;/code&gt; 자동 생성 (새 저장소 첫 명령)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/statusline&lt;/code&gt;&lt;/td&gt;&lt;td&gt;하단 상태줄(컨텍스트·비용·git 브랜치·모델)을 자연어로 설정&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/insights&lt;/code&gt;&lt;/td&gt;&lt;td&gt;내 사용 패턴·마찰 지점을 스캔해 리포트(HTML) 생성&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/powerup&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Claude Code 기능을 짧은 인터랙티브 레슨(약 30분)으로 학습&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/team-onboarding&lt;/code&gt;&lt;/td&gt;&lt;td&gt;최근 30일 이력으로 팀 온보딩용 &lt;code&gt;ONBOARDING.md&lt;/code&gt; 생성&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/radio&lt;/code&gt;&lt;/td&gt;&lt;td&gt;프롬프트에 입력하면 유튜브 BGM 재생 (작업용 배경음악)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/grill-me&lt;/code&gt;&lt;/td&gt;&lt;td&gt;계획·설계를 &lt;strong&gt;공유 이해에 도달할 때까지 집요하게 인터뷰&lt;/strong&gt; (질문 하나씩 + 추천답변)&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;  Caveman Mode — 토큰 절약&lt;/strong&gt; — 원시인처럼 &lt;strong&gt;군더더기 빼고 핵심만&lt;/strong&gt; 답하게 하는 모드. 답이 길수록 청구서가 두꺼워지는데, 짧게 답하도록 강제하면 평균 &lt;strong&gt;65% 토큰 절감&lt;/strong&gt;(때론 정확도도 오름). &lt;code&gt;/caveman&lt;/code&gt;으로 켜고 &lt;code&gt;stop caveman&lt;/code&gt;(노말모드)으로 끈다.&lt;/div&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;  skills.sh &amp;   PICKS(스킬 8선)&lt;/strong&gt; — AI 에이전트용 &lt;strong&gt;재사용 스킬&lt;/strong&gt;을 모아둔 오픈 디렉터리. &lt;code&gt;npx skills add &amp;lt;owner/repo&amp;gt;&lt;/code&gt; 한 줄로 설치한다. &quot;매일 손이 가는 스킬 8선&quot;(Karpathy Guidelines·Superpowers·Humanizer 등) 같은 큐레이션도 있어, 필요한 절차적 지식을 골라 붙일 수 있다.&lt;/div&gt;

    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;  GitHub × Claude Code&lt;/strong&gt; — 저장소 만들기 → &lt;code&gt;clone&lt;/code&gt; → 커밋·&lt;code&gt;push&lt;/code&gt; → PR까지의 흐름을 익혔다. 압권은 &lt;strong&gt;PR·이슈 댓글에 &lt;code&gt;@claude&lt;/code&gt;만 적으면&lt;/strong&gt; GitHub Action이 자동으로 코드를 고쳐 PR을 올려주는 것(&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; 시크릿 등록 필요).&lt;/div&gt;

    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;  TIPS(단축키·프롬프트) ·   HORROR&lt;/strong&gt; — &lt;code&gt;Shift+Tab&lt;/code&gt;(권한 모드 순환)·&lt;code&gt;!&lt;/code&gt;(셸 즉시 실행)·&lt;code&gt;@&lt;/code&gt;(파일 첨부)·&lt;code&gt;ultrathink&lt;/code&gt;(그 질문만 깊게) 같은 단축키/프롬프트 꿀팁 모음. HORROR는 재미로 넣은 이스터에그 카드다.   &lt;strong&gt;MCP&lt;/strong&gt;는 USB-C처럼 외부 도구(노션·카카오 등)를 통일 규격으로 연결하는 것으로, 오늘 자동화(아래)에서 직접 사용했다.&lt;/div&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;  bkit — 프로세스가 있는 AI 개발(플러그인)&lt;/strong&gt; — &quot;AI한테 그냥 시키는 개발&quot;을 &lt;strong&gt;PDCA(Plan-Do-Check-Act)&lt;/strong&gt; 프로세스가 있는 개발로 바꿔주는 Claude Code 플러그인. 기능마다 문서(PRD·설계·분석·보고서)가 나오고, 코드가 그 문서와 맞는지 자동 검증한다(내부적으로 44스킬·34에이전트·11품질게이트 조합).
        &lt;pre style=&quot;margin-top:10px;&quot;&gt;claude plugin install bkit
/pdca pm my-feature   &lt;span class=&quot;c&quot;&gt;# PRD→설계→개발→검증→보고&lt;/span&gt;
/pdca team            &lt;span class=&quot;c&quot;&gt;# 전문가 에이전트 팀 병렬 소환&lt;/span&gt;&lt;/pre&gt;
        기능마다 PRD·설계 문서가 남아 &lt;strong&gt;팀/강의 프로젝트의 공유·리뷰·평가&lt;/strong&gt;에 그대로 쓸 수 있다는 점이 인상적이었다.&lt;/div&gt;

    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;정리하면&lt;/strong&gt; — 허브 화면의 &lt;strong&gt;15개 카드&lt;/strong&gt;를 한 바퀴 훑었다: &lt;code&gt;/advisor&lt;/code&gt;·&lt;code&gt;/init&lt;/code&gt;·&lt;code&gt;/statusline&lt;/code&gt;·&lt;code&gt;/insights&lt;/code&gt;·&lt;code&gt;/powerup&lt;/code&gt;·&lt;code&gt;/team-onboarding(ONBOARDING)&lt;/code&gt;·&lt;code&gt;/radio&lt;/code&gt;·&lt;code&gt;/grill-me&lt;/code&gt;· CAVEMAN· SKILLS.SH· PICKS· GITHUB· TIPS· HORROR· BKIT· MCP. 슬래시 명령어부터 토큰 절약·스킬 생태계·협업 자동화까지, Claude Code를 &quot;확장하는 도구&quot;가 이렇게 많다는 걸 이번에 제대로 알았다.&lt;/div&gt;

    &lt;h2&gt;  커스텀 훅 — Dangerous Command Guard&lt;/h2&gt;

    &lt;p&gt;오늘의 핵심 실습. &lt;strong&gt;Claude Code가 복구 불가능한 Bash 명령을 실행하려 할 때, 자동으로 가로채서 사용자가 그 명령을 직접 타이핑해야만 실행을 허용하는 안전 가드 훅&lt;/strong&gt;을 만들었다. 훅(Hook)은 Claude가 특정 행동을 하기 &lt;strong&gt;직전/직후&lt;/strong&gt;에 끼어드는 내 코드다.&lt;/p&gt;

    &lt;h3&gt;동작 방식&lt;/h3&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;시도&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Claude가 Bash 툴로 명령 실행을 시도&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;이벤트&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;PreToolUse 이벤트 발생 → 훅 스크립트 자동 실행&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;감지&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;위험 패턴 감지 시 실행 중단 + 터미널에 경고 출력&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;확인&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;사용자가 명령어를 그대로 타이핑하면 허용, 아니면 차단&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;기록&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;시도 결과(차단/허용)를 로그 파일에 남김&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;h3&gt;차단 대상 명령어&lt;/h3&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;패턴&lt;/th&gt;&lt;th&gt;위험 이유&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;rm -rf&lt;/code&gt;&lt;/td&gt;&lt;td&gt;파일·디렉토리 강제 삭제&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;git reset --hard&lt;/code&gt;&lt;/td&gt;&lt;td&gt;미저장 변경사항 전체 파기&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;git push --force&lt;/code&gt;&lt;/td&gt;&lt;td&gt;원격 히스토리 덮어쓰기&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;DROP TABLE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DB 테이블 삭제&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;DROP DATABASE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DB 전체 삭제&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h3&gt;구성 요소&lt;/h3&gt;
    &lt;pre&gt;~/.claude/
├── hooks/
│   └── dangerous_guard.py   &lt;span class=&quot;c&quot;&gt;# 훅 스크립트 (Python)&lt;/span&gt;
├── settings.json            &lt;span class=&quot;c&quot;&gt;# PreToolUse 이벤트 등록&lt;/span&gt;
└── logs/
    └── dangerous_guard.log  &lt;span class=&quot;c&quot;&gt;# 시도 기록&lt;/span&gt;&lt;/pre&gt;

    &lt;p&gt;훅은 &lt;code&gt;settings.json&lt;/code&gt;의 &lt;code&gt;PreToolUse&lt;/code&gt;에 &lt;strong&gt;Bash 툴을 matcher로&lt;/strong&gt; 걸어 등록한다. Bash 실행 시도마다 이 파이썬 스크립트가 먼저 돌아간다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;&quot;hooks&quot;&lt;/span&gt;: {
  &lt;span class=&quot;k&quot;&gt;&quot;PreToolUse&quot;&lt;/span&gt;: [
    {
      &lt;span class=&quot;k&quot;&gt;&quot;matcher&quot;&lt;/span&gt;: &lt;span class=&quot;v&quot;&gt;&quot;Bash&quot;&lt;/span&gt;,
      &lt;span class=&quot;k&quot;&gt;&quot;hooks&quot;&lt;/span&gt;: [
        {
          &lt;span class=&quot;k&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;v&quot;&gt;&quot;command&quot;&lt;/span&gt;,
          &lt;span class=&quot;k&quot;&gt;&quot;command&quot;&lt;/span&gt;: &lt;span class=&quot;v&quot;&gt;&quot;python \&quot;C:/Users/Har06/.claude/hooks/dangerous_guard.py\&quot;&quot;&lt;/span&gt;
        }
      ]
    }
  ]
}&lt;/pre&gt;

    &lt;h3&gt;확인 방식 · 로그&lt;/h3&gt;
    &lt;p&gt;사용자가 위험 명령어를 &lt;strong&gt;토씨 하나 안 틀리고 그대로 타이핑&lt;/strong&gt;해야 실행이 허용된다. 틀리거나 빈 입력이면 차단된다.&lt;/p&gt;
    &lt;pre&gt;⚠️  위험 명령어 감지: [rm -rf]
   명령어: rm -rf ./dist

실행하려면 명령어를 그대로 입력하세요 (취소: Enter만 누르기):
&amp;gt; rm -rf ./dist     &lt;span class=&quot;c&quot;&gt;← 정확히 입력 시 허용&lt;/span&gt;&lt;/pre&gt;
    &lt;pre&gt;2026-07-24 14:32:11 | BLOCKED | rm -rf ./dist
2026-07-24 14:35:02 | ALLOWED | git reset --hard HEAD~1&lt;/pre&gt;

    &lt;h3&gt;핵심 기술&lt;/h3&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;내용&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;이벤트&lt;/td&gt;&lt;td&gt;&lt;code&gt;PreToolUse&lt;/code&gt; — Bash 툴 실행 &lt;strong&gt;전&lt;/strong&gt;에 가로채기&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;언어&lt;/td&gt;&lt;td&gt;Python&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;입력&lt;/td&gt;&lt;td&gt;stdin으로 JSON 수신 (&lt;code&gt;tool_input.command&lt;/code&gt; 필드)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;콘솔 입력&lt;/td&gt;&lt;td&gt;&lt;code&gt;CONIN$&lt;/code&gt; 직접 열기 (stdin이 이미 소비된 상태에서 사용자 입력 받기)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;종료 코드&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt; = 허용, &lt;code&gt;2&lt;/code&gt; = 차단&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;막혔던 지점&lt;/strong&gt; — stdin으로 이미 JSON을 다 읽어버린 뒤라, 같은 stdin으로 다시 사용자 키 입력을 받으려 하면 안 된다. 그래서 Windows 콘솔의 입력 버퍼인 &lt;code&gt;CONIN$&lt;/code&gt;를 &lt;strong&gt;직접 열어서&lt;/strong&gt; 타이핑을 받아야 했다. 종료 코드로 허용/차단이 갈린다는 것(&lt;code&gt;2&lt;/code&gt;면 Claude 쪽에서 실행이 막힘)도 이 실습으로 확실히 이해했다.&lt;/div&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;솔직한 후기&lt;/strong&gt; — 훅 실습을 로드맵 20번부터 끝까지 마저 진행했는데, 실행이 너무너무 느려서 훅이 제대로 걸린 건지 애매했다 ㅠㅠ. 개념과 흐름은 확실히 잡았지만, 실제 반응 속도 때문에 &quot;정말 동작하는가&quot;를 눈으로 확신하긴 어려웠던 부분이 아쉬웠다.&lt;/div&gt;

    &lt;h2&gt;  커스텀 플러그인 제작&lt;/h2&gt;

    &lt;p&gt;훅 하나로 끝이 아니라, &lt;strong&gt;스킬 · 훅 · 에이전트 · MCP&lt;/strong&gt;를 한 묶음으로 담은 &lt;strong&gt;커스텀 플러그인&lt;/strong&gt;을 만들어봤다. 여러 요소를 조합해 하나의 기능 패키지로 굳히고, &lt;strong&gt;마켓플레이스 배포 방식&lt;/strong&gt;으로 배포까지 진행했다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;구성 요소&lt;/th&gt;&lt;th&gt;역할&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;스킬(Skill)&lt;/td&gt;&lt;td&gt;반복 작업을 &lt;code&gt;/명령어&lt;/code&gt;로 굳혀 재사용&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;훅(Hook)&lt;/td&gt;&lt;td&gt;특정 행동 직전/직후에 내 코드가 자동 개입&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;에이전트(Agent)&lt;/td&gt;&lt;td&gt;목적별 서브에이전트에게 작업을 통째로 위임&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;MCP&lt;/td&gt;&lt;td&gt;노션·카카오 등 외부 도구를 통일 규격으로 연결&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;느낀 점&lt;/strong&gt; — 스킬·훅·에이전트·MCP가 따로 놀던 개념이었는데, 플러그인이라는 그릇에 &lt;strong&gt;한꺼번에 담아 배포&lt;/strong&gt;할 수 있다는 걸 알고 나니 &quot;내 워크플로 전체를 하나로 패키징한다&quot;는 그림이 잡혔다. 마켓플레이스로 배포한다는 건, 결국 내가 만든 도구를 남도 쓸 수 있게 &lt;strong&gt;상품처럼 유통&lt;/strong&gt;한다는 의미였다.&lt;/div&gt;

    &lt;h3&gt;실제 배포 &amp; 설치 방법&lt;/h3&gt;
    &lt;p&gt;Claude Code의 마켓플레이스는 특별한 서버가 아니라 &lt;strong&gt;깃허브 저장소 하나&lt;/strong&gt;다. 플러그인 파일과 &lt;code&gt;marketplace.json&lt;/code&gt;을 담은 repo를 푸시하면 그 자체가 배포다. 오늘 만든 &lt;code&gt;daily-log&lt;/code&gt; 플러그인을 &lt;code&gt;yundlab/claude-marketplace&lt;/code&gt; 저장소에 올렸고, 다른 사람은 아래 두 줄로 바로 설치해 쓸 수 있다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 1) 마켓플레이스(깃허브 repo) 등록&lt;/span&gt;
/plugin marketplace add yundlab/claude-marketplace

&lt;span class=&quot;c&quot;&gt;# 2) 플러그인 설치&lt;/span&gt;
/plugin install daily-log@yundlab-marketplace&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;설치하면 &lt;code&gt;/daily-log&lt;/code&gt; 스킬, &lt;code&gt;journal-writer&lt;/code&gt; 에이전트, 노션 MCP 설정이 한꺼번에 딸려 온다. 사용 전 노션 Integration 토큰을 &lt;code&gt;NOTION_TOKEN&lt;/code&gt; 환경변수로 넣어주면(&lt;code&gt;setx NOTION_TOKEN &quot;ntn_xxx&quot;&lt;/code&gt;) 준비 끝. 내가 만든 자동화를 &lt;strong&gt;명령어 두 줄&lt;/strong&gt;로 남에게 그대로 넘길 수 있다는 게 신기했다.&lt;/div&gt;

    &lt;h2&gt;  MCP 연결 → 학습일지 자동화&lt;/h2&gt;

    &lt;p&gt;마지막으로, 재시작된 세션에서 &lt;code&gt;/mcp&lt;/code&gt;로 &lt;strong&gt;notion MCP가 초록불(연결됨)&lt;/strong&gt;인지 확인한 뒤 &lt;code&gt;/daily-log&lt;/code&gt;를 실행했다. 노션의 오늘 메모와 코드 변경을 읽어, 지금 보고 있는 이 학습일지 HTML을 &lt;strong&gt;자동으로&lt;/strong&gt; 만들고 커밋까지 이어지는 흐름이다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# MCP 연결 확인 → 자동화 스킬 실행&lt;/span&gt;
/mcp          &lt;span class=&quot;c&quot;&gt;# notion 초록불 확인&lt;/span&gt;
/daily-log    &lt;span class=&quot;c&quot;&gt;# 오늘 메모 + git 변경 → 학습일지 HTML 생성·커밋&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;오늘 배운 &lt;strong&gt;스킬 · 훅 · 에이전트 · MCP&lt;/strong&gt;가 결국 이 &lt;code&gt;/daily-log&lt;/code&gt; 한 줄에 전부 모여 있다. 노션 MCP로 메모를 읽고, 서브에이전트가 글을 쓰고, 스킬이 전체 절차를 묶고, 커밋 형식 검증 훅이 마지막을 지킨다 — 오늘 실습이 하나의 자동화로 합쳐지는 게 눈에 보였다.&lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;어제까지가 Claude Code를 &quot;쓰는&quot; 법이었다면, 오늘은 &lt;strong&gt;&quot;뜯어고쳐 내 것으로 만드는&quot;&lt;/strong&gt; 법을 배웠다. 위험 명령을 직접 타이핑해야만 통과시키는 안전 가드 훅을 만들면서, AI에게 자율권을 주되 &lt;strong&gt;돌이킬 수 없는 순간엔 사람이 한 번 더 확인&lt;/strong&gt;하게 만드는 하네스의 감각을 손으로 익혔다. 스킬·훅·에이전트·MCP를 플러그인으로 묶어 배포하고, 그 조합이 &lt;code&gt;/daily-log&lt;/code&gt; 자동화로 합쳐지는 걸 보며 &quot;도구를 만드는 사람&quot;의 시야가 조금 열린 하루였다. 훅 실행이 느려 아쉬웠던 부분은, 다음에 더 가벼운 구조로 다시 확인해볼 생각이다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ Claude Code &amp;nbsp;|&amp;nbsp; 커스텀 훅 &amp;nbsp;|&amp;nbsp; PreToolUse &amp;nbsp;|&amp;nbsp; 플러그인 &amp;nbsp;|&amp;nbsp; MCP &amp;nbsp;|&amp;nbsp; 자동화 &amp;nbsp;|&amp;nbsp; 학습일지&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>claude code</category>
      <category>MCP</category>
      <category>PreToolUse</category>
      <category>자동화</category>
      <category>커스텀 훅</category>
      <category>플러그인</category>
      <category>학습일지</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/25</guid>
      <comments>https://yundlab.tistory.com/25#entry25comment</comments>
      <pubDate>Tue, 28 Jul 2026 11:00:08 +0900</pubDate>
    </item>
    <item>
      <title>[260723 18일차] Streamlit &amp;middot; Gradio &amp;middot; Claude Code</title>
      <link>https://yundlab.tistory.com/23</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0jvT3/dJMcaaMUaRh/4iOQYjxwaArPQE12A7FGh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0jvT3/dJMcaaMUaRh/4iOQYjxwaArPQE12A7FGh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0jvT3/dJMcaaMUaRh/4iOQYjxwaArPQE12A7FGh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0jvT3%2FdJMcaaMUaRh%2F4iOQYjxwaArPQE12A7FGh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;

&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# AI 웹앱 · 도구 학습일지&lt;/span&gt;
        &lt;h1&gt;[Streamlit · Gradio · Claude Code]&lt;br&gt;Python 웹앱 만들기 + AI 코딩 CLI&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.23 · 학습 범위: Streamlit, Gradio, Claude Code(로드맵 01~19)&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;오늘은 &lt;strong&gt;&quot;내가 만든 AI를 화면으로 보여주는&quot;&lt;/strong&gt; 하루였다. Python만으로 웹 대시보드를 만드는 &lt;strong&gt;Streamlit&lt;/strong&gt;, 함수 하나로 웹앱을 뚝딱 만드는 &lt;strong&gt;Gradio&lt;/strong&gt;로 CCTV 실시간 탐지·번역기·YOLO 탐지기를 웹으로 띄웠다. 그리고 지금 이 실습을 도와준 도구인 &lt;strong&gt;Claude Code&lt;/strong&gt;(AI 코딩 CLI) 자체의 명령어·CLAUDE.md·프롬프트·협업까지 로드맵으로 정리했다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;Streamlit&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;st.empty&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;plotly&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;Gradio&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;gr.Interface&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;gr.Blocks&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;launch&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;웹앱 예제&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;CCTV 탐지&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;번역기&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;YOLO&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;Claude 명령어&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;/init&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;@파일&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;/model&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;CLAUDE.md&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;메모리&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;@import&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;200줄&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;프롬프트&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;위치지정&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;역할&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;단계분해&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  Streamlit — Python 대시보드&lt;/h2&gt;

    &lt;p&gt;&lt;strong&gt;Streamlit&lt;/strong&gt;은 Python만으로 웹 앱을 뚝딱 만드는 도구. HTML·CSS·JS를 몰라도 AI 데모·대시보드를 즉시 만들고, &lt;code&gt;streamlit run 파일.py&lt;/code&gt; 한 줄로 브라우저에서 실행된다.&lt;/p&gt;

    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 가상환경 활성화&lt;/span&gt;
conda activate pyweb310

&lt;span class=&quot;c&quot;&gt;# 설치&lt;/span&gt;
pip install streamlit
pip install ultralytics streamlit

&lt;span class=&quot;c&quot;&gt;# 실행&lt;/span&gt;
streamlit run v19_01_streamlit_b.py&lt;/pre&gt;

    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;함수&lt;/th&gt;&lt;th&gt;역할&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;st.set_page_config()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;페이지 레이아웃·제목 설정(코드 최상단)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;st.empty()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;나중에 채울 빈 공간(placeholder) 예약&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;st.columns(n)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;화면을 n개 컬럼으로 분할&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;placeholder.image()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;예약 공간에 이미지(프레임) 갱신&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;placeholder.plotly_chart()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;예약 공간에 Plotly 그래프 갱신&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h3&gt;① 기본 CCTV 실시간 탐지&lt;/h3&gt;
    &lt;p&gt;빈 액자(&lt;code&gt;st.empty()&lt;/code&gt;)를 만들어 두고, 루프에서 YOLO 탐지 결과를 그 액자에 &lt;strong&gt;한 장씩 덮어써&lt;/strong&gt; 슬라이드 쇼처럼 실시간 영상을 보여준다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; streamlit &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; st
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; ultralytics &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; YOLO
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; cv2

st.set_page_config(layout=&lt;span class=&quot;v&quot;&gt;&quot;wide&quot;&lt;/span&gt;)      &lt;span class=&quot;c&quot;&gt;# 넓게 (반드시 맨 위)&lt;/span&gt;
frame_placeholder = st.empty()          &lt;span class=&quot;c&quot;&gt;# 사진 갈아끼울 빈 액자&lt;/span&gt;
cap = cv2.VideoCapture(stream_url)
model = YOLO(&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;)

&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; cap.isOpened():
    success, frame = cap.read()
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;not&lt;/span&gt; success: &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
    annotated = model(frame)[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;].plot()
    frame_placeholder.image(annotated, channels=&lt;span class=&quot;v&quot;&gt;&quot;BGR&quot;&lt;/span&gt;)  &lt;span class=&quot;c&quot;&gt;# 액자 갱신&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;code&gt;set_page_config(layout=&quot;wide&quot;)&lt;/code&gt;는 반드시 코드 최상단에 있어야 동작한다. OpenCV는 색 순서가 BGR이라 &lt;code&gt;channels=&quot;BGR&quot;&lt;/code&gt;를 명시해야 Streamlit에서 색이 제대로 나온다. &lt;code&gt;st.empty()&lt;/code&gt; placeholder를 루프에서 &lt;code&gt;.image()&lt;/code&gt;로 계속 덮어써야 실시간 효과가 난다.&lt;/div&gt;

    &lt;h3&gt;② 컬럼 레이아웃 + 실시간 그래프&lt;/h3&gt;
    &lt;p&gt;화면을 좌우 2칸으로 쪼개 왼쪽엔 영상, 오른쪽엔 &lt;strong&gt;탐지 개수 막대 그래프&lt;/strong&gt;를 동시에 갱신한다. Plotly로 종류별 개수를 매 프레임 다시 그린다.&lt;/p&gt;
    &lt;pre&gt;col1, col2 = st.columns(&lt;span class=&quot;v&quot;&gt;2&lt;/span&gt;)          &lt;span class=&quot;c&quot;&gt;# 좌·우 2칸&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; col1: frame_placeholder = st.empty()
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; col2: chart_placeholder = st.empty()

&lt;span class=&quot;a&quot;&gt;@st.cache_resource&lt;/span&gt;                  &lt;span class=&quot;c&quot;&gt;# 무거운 모델은 1회만 로드&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;load_model&lt;/span&gt;(): &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; YOLO(&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;)
model = load_model()

&lt;span class=&quot;c&quot;&gt;# 탐지된 클래스 이름 모으기 → 개수 집계 → 막대그래프&lt;/span&gt;
labels = [model.names[&lt;span class=&quot;a&quot;&gt;int&lt;/span&gt;(c)] &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; c &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; results[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;].boxes.cls]
df = pd.DataFrame({&lt;span class=&quot;v&quot;&gt;&quot;Object&quot;&lt;/span&gt;: labels}).value_counts().reset_index(name=&lt;span class=&quot;v&quot;&gt;&quot;Count&quot;&lt;/span&gt;)
fig = px.bar(df, x=&lt;span class=&quot;v&quot;&gt;&quot;Object&quot;&lt;/span&gt;, y=&lt;span class=&quot;v&quot;&gt;&quot;Count&quot;&lt;/span&gt;, color=&lt;span class=&quot;v&quot;&gt;&quot;Object&quot;&lt;/span&gt;, text=&lt;span class=&quot;v&quot;&gt;&quot;Count&quot;&lt;/span&gt;)
chart_placeholder.plotly_chart(fig, key=f&lt;span class=&quot;v&quot;&gt;&quot;chart_{time.time()}&quot;&lt;/span&gt;)&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;code&gt;@st.cache_resource&lt;/code&gt;는 무거운 AI 모델을 &lt;strong&gt;딱 한 번만&lt;/strong&gt; 불러온다(화면 조작으로 스크립트가 재실행돼도 캐시에서 즉시 반환 → 안 쓰면 매번 새로 읽어 느림). Plotly 차트는 매 프레임 &lt;code&gt;key=f&quot;chart_{time.time()}&quot;&lt;/code&gt;로 &lt;strong&gt;고유 key&lt;/strong&gt;를 줘야 위젯 충돌이 안 난다.&lt;/div&gt;

    &lt;h2&gt;  Gradio — 함수 하나로 웹앱&lt;/h2&gt;

    &lt;p&gt;&lt;strong&gt;Gradio&lt;/strong&gt;는 Python 함수만 있으면 웹 화면을 만들어주는 도구. HTML·CSS 없이 AI 데모를 만들고 &lt;code&gt;launch(share=True)&lt;/code&gt;로 인터넷 주소까지 발급한다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 설치&lt;/span&gt;
pip install gradio deep-translator&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;⚠ import 하자마자 &lt;code&gt;ImportError: numpy.core.multiarray failed to import&lt;/code&gt;가 나면&lt;/strong&gt; — gradio 잘못이 아니라 &lt;strong&gt;numpy 2 버전 충돌&lt;/strong&gt;이다(numpy 2부터 &lt;code&gt;numpy.core&lt;/code&gt;→&lt;code&gt;numpy._core&lt;/code&gt;로 바뀜, 옛 pandas·matplotlib이 옛 이름을 부름). gradio는 내부에서 pandas를 불러오다 터지는 피해자. 해결: &lt;code&gt;pip install -U pandas matplotlib&lt;/code&gt;로 최신판으로 갈아끼운다(numpy를 1.x로 내리는 건 비권장).&lt;/div&gt;

    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;파라미터&lt;/th&gt;&lt;th&gt;역할&lt;/th&gt;&lt;th&gt;예시&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;fn&lt;/code&gt;&lt;/td&gt;&lt;td&gt;실행할 함수&lt;/td&gt;&lt;td&gt;&lt;code&gt;fn=say_hello&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;inputs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;입력 컴포넌트&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;text&quot;&lt;/code&gt;, &lt;code&gt;gr.Image(type=&quot;numpy&quot;)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;outputs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;출력 컴포넌트&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;text&quot;&lt;/code&gt;, &lt;code&gt;gr.Image()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;launch(share=True)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;공개 URL 생성(72시간)&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h3&gt;① 기본 웹앱 (Hello)&lt;/h3&gt;
    &lt;p&gt;함수 정의 → &lt;code&gt;gr.Interface&lt;/code&gt; 연결 → &lt;code&gt;launch&lt;/code&gt; 세 단계로 끝나는 가장 쉬운 첫 웹앱.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;say_hello&lt;/span&gt;(name):
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;v&quot;&gt;&quot;Hello, &quot;&lt;/span&gt; + name

gr.Interface(fn=say_hello, inputs=&lt;span class=&quot;v&quot;&gt;&quot;text&quot;&lt;/span&gt;, outputs=&lt;span class=&quot;v&quot;&gt;&quot;text&quot;&lt;/span&gt;).launch(share=&lt;span class=&quot;k&quot;&gt;True&lt;/span&gt;)&lt;/pre&gt;

    &lt;h3&gt;② 번역 웹앱&lt;/h3&gt;
    &lt;p&gt;구글 번역 라이브러리에 Gradio 화면만 입혀 5줄로 완성한 영한 번역기.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; deep_translator &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; GoogleTranslator
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;trans_en_to_ko&lt;/span&gt;(text):
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; GoogleTranslator(source=&lt;span class=&quot;v&quot;&gt;&quot;en&quot;&lt;/span&gt;, target=&lt;span class=&quot;v&quot;&gt;&quot;ko&quot;&lt;/span&gt;).translate(text)

gr.Interface(fn=trans_en_to_ko, inputs=&lt;span class=&quot;v&quot;&gt;&quot;text&quot;&lt;/span&gt;, outputs=&lt;span class=&quot;v&quot;&gt;&quot;text&quot;&lt;/span&gt;,
             title=&lt;span class=&quot;v&quot;&gt;&quot;번역기&quot;&lt;/span&gt;, description=&lt;span class=&quot;v&quot;&gt;&quot;영어 → 한국어&quot;&lt;/span&gt;).launch()&lt;/pre&gt;

    &lt;h3&gt;③ YOLO 이미지 탐지&lt;/h3&gt;
    &lt;p&gt;사진을 올리면 YOLO가 사물을 찾아 박스를 그려준다. &lt;code&gt;gr.Image(type=&quot;numpy&quot;)&lt;/code&gt;로 이미지를 숫자 배열로 받아 모델에 바로 넘긴다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;det_image&lt;/span&gt;(image):
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; model(image)[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;].plot()

gr.Interface(fn=det_image, inputs=gr.Image(type=&lt;span class=&quot;v&quot;&gt;&quot;numpy&quot;&lt;/span&gt;), outputs=gr.Image(),
             title=&lt;span class=&quot;v&quot;&gt;&quot;YOLO 이미지 객체 탐지&quot;&lt;/span&gt;).launch()&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;code&gt;gr.Image(type=&quot;numpy&quot;)&lt;/code&gt; — &lt;code&gt;type&lt;/code&gt;을 안 적으면 PIL 객체로 들어가 오류가 난다. YOLO엔 숫자 배열(numpy)로 넘겨야 한다. 출력의 &lt;code&gt;gr.Image()&lt;/code&gt;는 배열도 자동으로 그림으로 표시한다.&lt;/div&gt;

    &lt;h3&gt;④ gr.Blocks 통합 앱 (탭 3개)&lt;/h3&gt;
    &lt;p&gt;인사·번역·YOLO를 &lt;strong&gt;탭 3개&lt;/strong&gt;로 묶어 한 앱에 담았다. 함수는 그대로 두고 화면 짜는 부분만 &lt;code&gt;gr.Blocks&lt;/code&gt;로 바꾼다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; gr.Blocks() &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; gr_web:
    gr.Markdown(&lt;span class=&quot;v&quot;&gt;&quot;# 내가 만든 AI 앱 모음&quot;&lt;/span&gt;)
    &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; gr.Tab(&lt;span class=&quot;v&quot;&gt;&quot;번역&quot;&lt;/span&gt;):
        en = gr.Textbox(label=&lt;span class=&quot;v&quot;&gt;&quot;영어&quot;&lt;/span&gt;)
        ko = gr.Textbox(label=&lt;span class=&quot;v&quot;&gt;&quot;한국어&quot;&lt;/span&gt;)
        gr.Button(&lt;span class=&quot;v&quot;&gt;&quot;번역&quot;&lt;/span&gt;).click(trans_en_to_ko, en, ko)  &lt;span class=&quot;c&quot;&gt;# (함수, 입력칸, 출력칸)&lt;/span&gt;
gr_web.launch()&lt;/pre&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;비교&lt;/th&gt;&lt;th&gt;gr.Interface&lt;/th&gt;&lt;th&gt;gr.Blocks&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;화면&lt;/td&gt;&lt;td&gt;Gradio가 알아서&lt;/td&gt;&lt;td&gt;내가 쓴 순서대로 배치&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;기능 개수&lt;/td&gt;&lt;td&gt;1개&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;code&gt;.click()&lt;/code&gt;으로 내가 정함&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;자주 하는 실수&lt;/strong&gt; — 탭마다 칸 이름을 똑같이 지으면(&lt;code&gt;name_box&lt;/code&gt;) 나중 것이 앞 것을 덮어써 엉뚱한 칸에 결과가 나온다. &lt;code&gt;.click()&lt;/code&gt;은 칸을 만든 &lt;code&gt;with&lt;/code&gt; 블록 안에, &lt;code&gt;launch()&lt;/code&gt;는 &lt;code&gt;with&lt;/code&gt; 밖에 둔다. 버튼만 만들고 &lt;code&gt;.click()&lt;/code&gt;을 안 붙이면 눌러도 아무 일이 없다.&lt;/div&gt;

    &lt;h2&gt;  Claude Code — AI 코딩 CLI (로드맵 01~19)&lt;/h2&gt;

    &lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;는 Anthropic이 만든 터미널용 AI 코딩 도구. 지금 이 정리도 Claude Code로 만들고 있어서, 도구 자체를 제대로 뜯어봤다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 01 설치 (Node.js 18+)&lt;/span&gt;
npm install -g @anthropic-ai/claude-code
claude --version   &lt;span class=&quot;c&quot;&gt;# 확인 후 claude 로 실행&lt;/span&gt;&lt;/pre&gt;

    &lt;h3&gt;02~03 · 명령어&lt;/h3&gt;
    &lt;p&gt;세션 안에서 쓰는 슬래시 명령어. 새 프로젝트는 &lt;code&gt;/init&lt;/code&gt;으로 시작한다.&lt;/p&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;명령어&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/init&lt;/code&gt;&lt;/td&gt;&lt;td&gt;CLAUDE.md 자동 생성(프로젝트 설정)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/help&lt;/code&gt; · &lt;code&gt;/clear&lt;/code&gt;&lt;/td&gt;&lt;td&gt;도움말 · 대화 초기화&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/compact&lt;/code&gt;&lt;/td&gt;&lt;td&gt;긴 대화 요약·압축(컨텍스트 절약)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;@파일&lt;/code&gt;&lt;/td&gt;&lt;td&gt;특정 파일만 수정 지정(&lt;code&gt;@src/main.py&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;!&lt;/code&gt; prefix&lt;/td&gt;&lt;td&gt;터미널 명령 즉시 실행(&lt;code&gt;!ls -la&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;/model&lt;/code&gt; · &lt;code&gt;/btw&lt;/code&gt;&lt;/td&gt;&lt;td&gt;모델 전환 · 작업 중단 없이 간단 질문&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;h3&gt;04 · 14 · 15 — 작동 모드 · 모델 · Effort&lt;/h3&gt;
    &lt;p&gt;&lt;code&gt;Shift+Tab&lt;/code&gt;으로 &lt;strong&gt;작동 모드 3가지&lt;/strong&gt;를 순환한다: &lt;code&gt;default&lt;/code&gt;(매번 확인) → &lt;code&gt;auto&lt;/code&gt;(자동 수행) → &lt;code&gt;plan&lt;/code&gt;(계획 먼저 작성 후 승인). 작업 성격에 따라 &lt;strong&gt;모델&lt;/strong&gt;(Opus=복잡, Sonnet=일반, Haiku=빠름)과 &lt;strong&gt;Effort&lt;/strong&gt;(low~max, 높을수록 깊은 사고·토큰↑)를 고른다.&lt;/p&gt;

    &lt;div class=&quot;callout yellow&quot;&gt;
        &lt;strong&gt;  고급 설정 — 자율권과 안전&lt;/strong&gt; — Claude에게 주는 &lt;strong&gt;자율권을 조절하는 장치&lt;/strong&gt;다. 신입에게 &quot;매번 확인받고 진행&quot;시키다 신뢰가 쌓이면 &quot;알아서 해&quot;로 바꾸는 것과 같은데, &lt;strong&gt;자율권 ↑ = 속도 ↑ = 위험 ↑&lt;/strong&gt;이라 실수를 되돌리기 어려워진다. 극단이 &lt;code&gt;claude --dangerously-skip-permissions&lt;/code&gt;(bypassPermissions) — 모든 작업을 확인 없이 자동 실행하므로 &lt;strong&gt;신뢰 가능한 격리 환경(도커·VM)에서만&lt;/strong&gt; 써야 한다. &lt;em&gt;&quot;큰 힘에는 큰 책임이 따른다.&quot;&lt;/em&gt;&lt;/div&gt;

    &lt;h3&gt;05 — 이미지 사용&lt;/h3&gt;
    &lt;p&gt;스크린샷을 채팅창에 드롭하면 분석한다. &quot;이 UI 오류 고쳐줘&quot;, &quot;이 시안대로 HTML 만들어줘&quot;, &quot;이 에러 원인이 뭐야?&quot;처럼 화면을 그대로 넘길 수 있다.&lt;/p&gt;

    &lt;h3&gt;06 · 07 — 용어&lt;/h3&gt;
    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;용어&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Context Window&lt;/td&gt;&lt;td&gt;AI가 한 번에 처리할 수 있는 최대 텍스트 길이&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Token&lt;/td&gt;&lt;td&gt;텍스트 처리 최소 단위(약 0.75단어)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Hallucination&lt;/td&gt;&lt;td&gt;사실 아닌 내용을 자신 있게 생성하는 현상&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;MCP&lt;/td&gt;&lt;td&gt;USB-C처럼 통일 규격으로 외부 도구 연결&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;프롬프트 엔지니어링 vs 컨텍스트 엔지니어링&lt;/strong&gt; — 전자는 &lt;strong&gt;무엇을 물어볼지&lt;/strong&gt;(질문·지시를 잘 쓰기), 후자는 &lt;strong&gt;어떤 정보를 줄지&lt;/strong&gt;(CLAUDE.md·&lt;code&gt;@파일&lt;/code&gt;로 대화에 담을 정보를 구성)에 집중한다.&lt;/div&gt;

    &lt;h3&gt;08 — AI 도입 3단계 여정&lt;/h3&gt;
    &lt;p&gt;새 도구를 채택할 때 누구나 거치는 과정(Mitchell Hashimoto): ① &lt;strong&gt;비효율의 기간&lt;/strong&gt;(기존보다 느리고 어색) → ② &lt;strong&gt;적절성의 기간&lt;/strong&gt;(비슷한 효율) → ③ &lt;strong&gt;발견의 기간&lt;/strong&gt;(워크플로가 바뀌는 생산성 도약).&lt;/p&gt;

    &lt;h3&gt;  하네스 엔지니어링 (Harness Engineering)&lt;/h3&gt;
    &lt;p&gt;&lt;strong&gt;하네스(Harness)&lt;/strong&gt;는 AI 에이전트가 &lt;strong&gt;오랜 시간 일할 때 방향을 잡아주고 실수를 감지하는 관리 기술&lt;/strong&gt;이다. 마치 등산 안전줄처럼 — AI가 자유롭게 일하되, 위험한 순간엔 자동으로 제동이 걸리게 만드는 &lt;strong&gt;제어 시스템&lt;/strong&gt;이다. 프롬프트/컨텍스트 엔지니어링이 &quot;한 번의 요청을 잘 만드는&quot; 것이라면, 하네스 엔지니어링은 &quot;장시간 자율 실행되는 에이전트를 안전하게 굴리는&quot; 상위 관점이다.&lt;/p&gt;
    &lt;div class=&quot;callout green&quot;&gt;
        참고: Anthropic — &lt;em&gt;Effective Harnesses for Long-Running Agents&lt;/em&gt;. 그리고 하네스 엔지니어링 이후, 요즘은 &lt;strong&gt;루프 엔지니어링(Loop Engineering)&lt;/strong&gt;이라는 개념도 새로 등장했다. 에이전트가 &lt;strong&gt;관찰 → 행동 → 피드백&lt;/strong&gt;을 반복하는 &lt;strong&gt;실행 루프 자체를 설계·최적화&lt;/strong&gt;하는 접근으로, 언제 멈추고 언제 다시 시도할지, 어떤 신호로 루프를 조율할지에 초점을 둔다.
    &lt;/div&gt;

    &lt;h3&gt;09 · 10 · 11 — CLAUDE.md 메모리&lt;/h3&gt;
    &lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;는 모든 세션 시작 시 자동으로 컨텍스트에 로드된다. &lt;strong&gt;200줄 이하&lt;/strong&gt;로 유지해야 지침 준수율이 높다. 초과 내용은 &lt;code&gt;@import&lt;/code&gt;로 분리한다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# CLAUDE.md 안에서 다른 파일 참조&lt;/span&gt;
@docs/git-instructions.md
@.claude/rules/testing.md&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;넣을 것&lt;/strong&gt; — 빌드·테스트·린트 명령, 코드 스타일, 브랜치 규칙. &lt;strong&gt;뺄 것&lt;/strong&gt; — 코드 보면 아는 것, 자주 바뀌는 정보. 강조는 &lt;code&gt;IMPORTANT&lt;/code&gt;·&lt;code&gt;YOU MUST&lt;/code&gt;로. 개인 설정은 &lt;code&gt;CLAUDE.local.md&lt;/code&gt;(gitignore), 팀 공유는 git 커밋.&lt;/div&gt;

    &lt;h3&gt;12 · 13 · 16 — 프롬프트 잘 쓰는 법&lt;/h3&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;위치 지정&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;&quot;auth.js에서 비밀번호 찾기 버튼 오류 수정&quot; — @파일로 콕 집기&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;역할 명확화&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;&quot;당신은 시니어 백엔드 엔지니어, 성능 최적화 중심으로&quot;&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;금지사항 명시&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;&quot;새 패키지 추가 금지, 동작하는 기능은 건드리지 말 것&quot;&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;단계별 분해&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;&quot;분석 → 방법 설명 → 코드 작성 → 저장 순서로&quot;&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;검증 수단 제공&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;&quot;수정 후 자동 검사 돌려 기존 기능 문제없는지 확인&quot;&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 구조화 템플릿&lt;/span&gt;
[ROLE: 시니어 백엔드 엔지니어]
[GOAL: 로그인 버그 수정]
[CONSTRAINTS: Python만, 새 패키지 금지]
[CONTEXT: auth.py 245번 줄 오류]
[QUESTION: 원인과 수정 방법은?]&lt;/pre&gt;

    &lt;h3&gt;17 — 세션 관리&lt;/h3&gt;
    &lt;p&gt;종료했다 다시 시작해도 문맥을 유지한다. &lt;code&gt;claude --resume&lt;/code&gt;으로 최근 세션 목록에서 골라 이어가거나, &lt;code&gt;--continue&lt;/code&gt;로 최근 세션을 바로 잇는다.&lt;/p&gt;

    &lt;h3&gt;18 · 19 — 스킬 &amp; 협업&lt;/h3&gt;
    &lt;p&gt;자주 쓰는 작업을 &lt;code&gt;.claude/skills/이름.md&lt;/code&gt;에 저장하면 &lt;code&gt;/이름&lt;/code&gt;으로 바로 실행하는 &lt;strong&gt;나만의 슬래시 명령어&lt;/strong&gt;가 된다. 나아가 Claude App + Chrome 확장 + MCP를 연결하면 &quot;뉴스 요약해서 카톡으로 보내줘&quot; 같은 &lt;strong&gt;복합 자동화&lt;/strong &gt;도 가능하다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# .claude/skills/translate-ko.md → /translate-ko 로 실행&lt;/span&gt;
주어진 텍스트를 자연스러운 한국어로 번역합니다.
- 구어체보다 문어체 우선
- 전문 용어는 원문 병기&lt;/pre&gt;

    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;  직접 스킬 만들기 실습&lt;/strong&gt; — 이론만 보지 않고, &lt;code&gt;.claude/skills/&lt;/code&gt; 폴더에 &lt;code&gt;이름.md&lt;/code&gt; 파일을 만들어 작업 지시를 적고 &lt;code&gt;/이름&lt;/code&gt;으로 실행되는 &lt;strong&gt;나만의 스킬&lt;/strong&gt;을 직접 만들어봤다. 자주 반복하는 프롬프트를 한 번 스킬로 굳혀 두니, 다음부턴 명령어 하나로 똑같은 작업을 재현할 수 있어서 편했다.&lt;/div&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;  스킬도 사고판다&lt;/strong&gt; — 잘 만든 스킬은 상품이 되기도 한다. &lt;a href=&quot;https://www.skills.ag/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;skills.ag&lt;/a&gt; 같은 마켓에서는 검증된 AI 에이전트 스킬을 &lt;strong&gt;판매·구매&lt;/strong&gt;한다. &quot;반복 작업을 절차적 지식으로 굳혀 재사용한다&quot;는 스킬의 개념이, 이제 하나의 &lt;strong&gt;생태계·시장&lt;/strong&gt;으로 커지고 있다는 게 인상적이었다.&lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;Streamlit·Gradio를 보면서 &quot;AI 모델을 만드는 것&quot;과 &quot;그걸 사람이 쓰게 만드는 것&quot;은 다른 일이라는 걸 느꼈다. 함수 하나에 &lt;code&gt;gr.Interface&lt;/code&gt; 몇 줄만 붙여도 친구에게 링크로 보낼 수 있는 웹앱이 된다는 게 특히 인상적이었다. 그리고 그동안 무심코 써온 &lt;strong&gt;Claude Code&lt;/strong&gt; 자체를 뜯어보니, 결국 핵심은 &quot;무엇을 시키느냐(프롬프트)&quot;보다 &quot;어떤 맥락을 주느냐(CLAUDE.md·@파일)&quot;라는 컨텍스트 엔지니어링이었다. 도구를 아는 만큼 더 잘 쓸 수 있다는 걸 체감한 하루였다. 다음엔 만든 앱들을 실제로 배포해 링크로 공유해볼 생각이다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ Streamlit &amp;nbsp;|&amp;nbsp; Gradio &amp;nbsp;|&amp;nbsp; Claude Code &amp;nbsp;|&amp;nbsp; 웹앱 &amp;nbsp;|&amp;nbsp; AI &amp;nbsp;|&amp;nbsp; 학습일지&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/23</guid>
      <comments>https://yundlab.tistory.com/23#entry23comment</comments>
      <pubDate>Thu, 23 Jul 2026 16:05:11 +0900</pubDate>
    </item>
    <item>
      <title>[260722 17일차] OpenAPI &amp;middot; HuggingFace &amp;middot; Transformers &amp;middot; OCR</title>
      <link>https://yundlab.tistory.com/22</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9pVB/dJMcabrui5y/jXO5oJKG22Mk4fvyWwHFO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9pVB/dJMcabrui5y/jXO5oJKG22Mk4fvyWwHFO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9pVB/dJMcabrui5y/jXO5oJKG22Mk4fvyWwHFO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9pVB%2FdJMcabrui5y%2FjXO5oJKG22Mk4fvyWwHFO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;

&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 150px !important;
    min-width: 150px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# AI 활용 학습일지&lt;/span&gt;
        &lt;h1&gt;[OpenAPI · HuggingFace · Transformers · OCR]&lt;br&gt;공공 API · LLM · NLP · 문자 인식&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.22 · 학습 범위: 공공 OpenAPI(ITS CCTV), HuggingFace, Transformers, OCR&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;오늘은 &lt;strong&gt;&quot;이미 만들어진 AI·데이터를 가져다 쓰는&quot;&lt;/strong&gt; 하루였다. 공공기관의 &lt;strong&gt;OpenAPI&lt;/strong&gt;로 전국 CCTV 영상을 받아 YOLO로 분석하고, &lt;strong&gt;HuggingFace&lt;/strong&gt;로 LLM 채팅과 이미지 생성을, &lt;strong&gt;Transformers&lt;/strong&gt;로 문장 유사도·감정분석·요약·번역을, 마지막으로 &lt;strong&gt;OCR&lt;/strong&gt;로 이미지 속 글자까지 뽑아봤다. 모델을 직접 학습하지 않아도 API·라이브러리 몇 줄로 이만한 걸 한다는 게 신기했다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;공공 API&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;apiKey&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;urllib&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;json_normalize&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;LLM 채팅&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;InferenceClient&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;DeepSeek&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;이미지 생성&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;FLUX.1&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;text_to_image&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;NLP&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;pipeline&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;유사도&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;감정분석&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;요약·번역&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;t5-small&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;deep_translator&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;문자 인식&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Tesseract&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;pytesseract&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  공공 OpenAPI — ITS CCTV&lt;/h2&gt;

    &lt;p&gt;&lt;strong&gt;OpenAPI&lt;/strong&gt;는 정부·공공기관 데이터 창고의 자동문 같은 것. 사람이 직접 가지 않고 프로그램이 &lt;strong&gt;인증키(열쇠)&lt;/strong&gt;를 들고 필요한 데이터만 꺼내온다. 오늘은 국토교통부 &lt;strong&gt;ITS(지능형교통시스템)&lt;/strong&gt;에서 전국 도로 CCTV 영상 주소를 받아왔다.&lt;/p&gt;

    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 설치 (urllib·json은 파이썬 기본 내장)&lt;/span&gt;
pip install pandas ultralytics opencv-python&lt;/pre&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;  곁들여 배운 개념 — MCP&lt;/strong&gt; — AI에 새 능력을 더해주는 &quot;USB-C 같은 약속&quot;. 도구마다 다른 연결 방식을 하나의 표준으로 통일한 것으로, AI가 혼자 모르는 것도 도구(기상청·노션·캘린더 등)를 꽂으면 진짜 정보를 가져와 할루시네이션(그럴듯한 거짓말)을 줄인다. OpenAPI가 &quot;데이터를 직접 호출&quot;하는 방식이라면, MCP는 그걸 AI가 알아서 쓰게 연결하는 상위 개념.&lt;/div&gt;

    &lt;h3&gt;① API 키 · 요청 URL&lt;/h3&gt;
    &lt;p&gt;인증키를 발급받고, 도로 유형(&lt;code&gt;its&lt;/code&gt; 일반 / &lt;code&gt;ex&lt;/code&gt; 고속)·관심 영역(경위도 범위)·응답 형식(&lt;code&gt;json&lt;/code&gt;)을 붙여 요청 URL을 조립한다.&lt;/p&gt;
    &lt;pre&gt;url_cctv = (
    f&lt;span class=&quot;v&quot;&gt;&quot;https://openapi.its.go.kr:9443/cctvInfo&quot;&lt;/span&gt;
    f&lt;span class=&quot;v&quot;&gt;&quot;?apiKey={key}&amp;type={Type}&amp;cctvType=1&quot;&lt;/span&gt;
    f&lt;span class=&quot;v&quot;&gt;&quot;&amp;minX={minX}&amp;maxX={maxX}&amp;minY={minY}&amp;maxY={maxY}&quot;&lt;/span&gt;
    f&lt;span class=&quot;v&quot;&gt;&quot;&amp;getType={getType}&quot;&lt;/span&gt;
)&lt;/pre&gt;

    &lt;h3&gt;② 응답 처리 (urllib → JSON → DataFrame)&lt;/h3&gt;
    &lt;p&gt;URL로 요청을 보내 받은 응답을 &lt;strong&gt;bytes → str → dict → 표(DataFrame)&lt;/strong&gt; 순으로 가공한 뒤, 원하는 CCTV의 스트림 주소를 골라낸다.&lt;/p&gt;
    &lt;pre&gt;response = urllib.request.urlopen(url_cctv)          &lt;span class=&quot;c&quot;&gt;# 요청 → 응답&lt;/span&gt;
json_str = response.read().decode(&lt;span class=&quot;v&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;)         &lt;span class=&quot;c&quot;&gt;# bytes → str&lt;/span&gt;
json_object = json.loads(json_str)                  &lt;span class=&quot;c&quot;&gt;# str → dict&lt;/span&gt;
cctv_play = pd.json_normalize(json_object[&lt;span class=&quot;v&quot;&gt;&quot;response&quot;&lt;/span&gt;][&lt;span class=&quot;v&quot;&gt;&quot;data&quot;&lt;/span&gt;])  &lt;span class=&quot;c&quot;&gt;# dict → 표&lt;/span&gt;
test_url = cctv_play[&lt;span class=&quot;v&quot;&gt;&quot;cctvurl&quot;&lt;/span&gt;][&lt;span class=&quot;v&quot;&gt;77&lt;/span&gt;]                   &lt;span class=&quot;c&quot;&gt;# 77번 CCTV 선택&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;왜 두 번 변환?&lt;/strong&gt; — 서버는 전송용 &lt;strong&gt;bytes&lt;/strong&gt;(0·1)로 보내니 사람이 읽으려면 &lt;code&gt;decode&lt;/code&gt;로 &lt;strong&gt;str&lt;/strong&gt;(글자)로 풀고, 값을 꺼내려면 &lt;code&gt;json.loads&lt;/code&gt;로 &lt;strong&gt;dict&lt;/strong&gt;(사전)로 바꿔야 &lt;code&gt;[&quot;키&quot;]&lt;/code&gt;로 접근된다. CCTV 목록이 &lt;code&gt;response → data&lt;/code&gt; 안에 숨어 있어서 &lt;code&gt;pd.json_normalize&lt;/code&gt;로 평평한 표로 펴 인덱싱했다.&lt;/div&gt;

    &lt;h3&gt;③ 함수화 + YOLO 통합&lt;/h3&gt;
    &lt;p&gt;반복되는 API 호출을 &lt;code&gt;its_cctv(번호)&lt;/code&gt; 함수로 묶으니, 다른 파일에서 &lt;strong&gt;import 한 줄&lt;/strong&gt;로 CCTV 주소를 얻어 바로 YOLO에 연결할 수 있었다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; v15_03_cctv_its_def &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; its_cctv  &lt;span class=&quot;c&quot;&gt;# 만들어 둔 함수 재사용&lt;/span&gt;

test_url = its_cctv(&lt;span class=&quot;v&quot;&gt;50&lt;/span&gt;)              &lt;span class=&quot;c&quot;&gt;# 17줄 코드가 1줄로&lt;/span&gt;
cap = cv2.VideoCapture(test_url)
model = YOLO(&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;)
&lt;span class=&quot;c&quot;&gt;# while 루프에서 프레임마다 model(frame)로 실시간 탐지&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;함수화의 장점 — &lt;strong&gt;재사용&lt;/strong&gt;(import 한 줄), &lt;strong&gt;매개변수&lt;/strong&gt;(번호만 바꿔 다른 CCTV), &lt;strong&gt;가독성&lt;/strong&gt;(메인 로직에서 API 세부 숨김), &lt;strong&gt;유지보수&lt;/strong&gt;(주소·키 바뀌면 함수 1곳만 수정). 참고로 알라딘·기상청·카카오·네이버도 같은 방식(키 발급 → URL/헤더에 담아 요청 → JSON 파싱)으로 쓴다.&lt;/div&gt;

    &lt;h2&gt;  HuggingFace — LLM · 이미지 생성&lt;/h2&gt;

    &lt;p&gt;&lt;strong&gt;HuggingFace&lt;/strong&gt;는 전 세계 AI 모델·데이터셋을 무료 공유하는 최대 허브. &lt;code&gt;InferenceClient&lt;/code&gt; 한 줄로 GPT급 LLM이나 이미지 생성 모델을 바로 호출할 수 있다.&lt;/p&gt;

    &lt;pre&gt;pip install huggingface_hub   &lt;span class=&quot;c&quot;&gt;# HuggingFace 공식 SDK → InferenceClient·model_info·login 등 일체 제공&lt;/span&gt;&lt;/pre&gt;

    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;  토큰은 &lt;code&gt;.env&lt;/code&gt;에&lt;/strong&gt; — HuggingFace 토큰은 &lt;strong&gt;절대 코드·주석에 직접 쓰지 말고&lt;/strong&gt; &lt;code&gt;.env&lt;/code&gt;(gitignore됨)에 저장한다. &lt;code&gt;load_dotenv()&lt;/code&gt;로 읽어 &lt;code&gt;os.environ[&quot;HF_TOKEN&quot;]&lt;/code&gt;으로 꺼내 쓴다. 키가 코드에 박히면 GitHub에 그대로 노출된다.&lt;/div&gt;

    &lt;h3&gt;① LLM 채팅 (DeepSeek)&lt;/h3&gt;
    &lt;p&gt;&lt;code&gt;chat.completions.create&lt;/code&gt;로 DeepSeek 모델에 질문을 보낸다. ChatGPT와 동일한 &lt;code&gt;messages&lt;/code&gt;(role + content) 패턴이다.&lt;/p&gt;
    &lt;pre&gt;client = InferenceClient(provider=&lt;span class=&quot;v&quot;&gt;&quot;auto&quot;&lt;/span&gt;, api_key=os.environ[&lt;span class=&quot;v&quot;&gt;'HF_TOKEN'&lt;/span&gt;])
completion = client.chat.completions.create(
    model=&lt;span class=&quot;v&quot;&gt;&quot;deepseek-ai/DeepSeek-V3.2:novita&quot;&lt;/span&gt;,  &lt;span class=&quot;c&quot;&gt;# :novita → 무료 라우팅&lt;/span&gt;
    messages=[{&lt;span class=&quot;v&quot;&gt;&quot;role&quot;&lt;/span&gt;: &lt;span class=&quot;v&quot;&gt;&quot;user&quot;&lt;/span&gt;, &lt;span class=&quot;v&quot;&gt;&quot;content&quot;&lt;/span&gt;: answer}],
)
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;(completion.choices[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;].message)   &lt;span class=&quot;c&quot;&gt;# 후보 중 첫 응답&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;code&gt;provider=&quot;auto&quot;&lt;/code&gt;는 가장 빠른 무료 서버를 자동 선택. LLM은 단일 질문이 아니라 &lt;strong&gt;대화 이력(messages 리스트)&lt;/strong&gt;을 입력받고, 답은 후보 N개 중 &lt;code&gt;choices[0].message&lt;/code&gt;로 꺼낸다.&lt;/div&gt;

    &lt;h3&gt;② 이미지 생성 (Text-to-Image, FLUX)&lt;/h3&gt;
    &lt;p&gt;텍스트 설명을 주면 &lt;code&gt;FLUX.1-dev&lt;/code&gt;가 그림을 그려 &lt;strong&gt;PIL.Image&lt;/strong&gt; 객체로 돌려준다. 그대로 &lt;code&gt;.save()&lt;/code&gt;하면 파일로 저장된다.&lt;/p&gt;
    &lt;pre&gt;image = client.text_to_image(answer, model=&lt;span class=&quot;v&quot;&gt;&quot;black-forest-labs/FLUX.1-dev&quot;&lt;/span&gt;)
image.save(&lt;span class=&quot;v&quot;&gt;&quot;tti_result.jpg&quot;&lt;/span&gt;)&lt;/pre&gt;
    &lt;div class=&quot;callout green&quot;&gt;프롬프트는 &lt;strong&gt;영어&lt;/strong&gt;로 쓰면 품질이 오른다. 스타일 키워드(&lt;code&gt;anime style, photorealistic&lt;/code&gt;)와 품질 키워드(&lt;code&gt;high quality, 4K, detailed&lt;/code&gt;)를 붙이면 좋다. 무료 서버 기준 1회 1~3분 걸린다.&lt;/div&gt;

    &lt;h2&gt;  Transformers — 자연어 처리(NLP)&lt;/h2&gt;

    &lt;p&gt;&lt;code&gt;pipeline&lt;/code&gt;은 모델 로드 + 토크나이저 + 후처리를 &lt;strong&gt;한 줄로 묶어주는&lt;/strong&gt; 진입점. 태스크 이름만 주면 알맞은 모델을 자동으로 붙여준다.&lt;/p&gt;

    &lt;pre&gt;pip install transformers
pip install -U &lt;span class=&quot;v&quot;&gt;&quot;transformers==4.46.3&quot;&lt;/span&gt;
pip install deep-translator&lt;/pre&gt;

    &lt;h3&gt;① 문장 유사도 (SentenceTransformer · 코사인)&lt;/h3&gt;
    &lt;p&gt;문장을 &lt;strong&gt;임베딩(숫자 벡터)&lt;/strong&gt;으로 바꾸고, 두 벡터 사잇각의 &lt;strong&gt;코사인 유사도&lt;/strong&gt;로 의미가 얼마나 비슷한지 수치화한다. 단어가 달라도 의미가 같으면 1에 가깝다.&lt;/p&gt;

    &lt;pre&gt;pip install sentence-transformers   &lt;span class=&quot;c&quot;&gt;# 문장 임베딩 전용 패키지&lt;/span&gt;&lt;/pre&gt;

    &lt;pre&gt;model = SentenceTransformer(&lt;span class=&quot;v&quot;&gt;&quot;all-MiniLM-L6-v2&quot;&lt;/span&gt;)   &lt;span class=&quot;c&quot;&gt;# 경량 임베딩 모델&lt;/span&gt;
emb1 = model.encode(sen1, convert_to_tensor=&lt;span class=&quot;k&quot;&gt;True&lt;/span&gt;)
emb2 = model.encode(sen2, convert_to_tensor=&lt;span class=&quot;k&quot;&gt;True&lt;/span&gt;)
cos_sim = util.pytorch_cos_sim(emb1, emb2)
&lt;span class=&quot;c&quot;&gt;# -1 반대 · 0 무관 · 1 완전히 동일&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;strong&gt;임베딩 = 문장을 의미 좌표로&lt;/strong&gt; — &quot;강아지&quot;와 &quot;개&quot;는 가까운 점, &quot;강아지&quot;와 &quot;비행기&quot;는 먼 점이 된다. 코사인 유사도는 두 화살표의 &lt;strong&gt;각도만&lt;/strong&gt; 보므로(길이 무시) 문장 길이에 영향받지 않는다. 실제로 어순만 다른 문장은 0.94, 무관한 문장은 0.15가 나왔다.&lt;/div&gt;

    &lt;h3&gt;② 감정 분석 (Sentiment)&lt;/h3&gt;
    &lt;p&gt;영어 문장을 &lt;strong&gt;POSITIVE / NEGATIVE&lt;/strong&gt;로 판별하고 신뢰도(0~1)를 준다.&lt;/p&gt;
    &lt;pre&gt;classifier = pipeline(&lt;span class=&quot;v&quot;&gt;&quot;sentiment-analysis&quot;&lt;/span&gt;)
results = classifier(&lt;span class=&quot;v&quot;&gt;&quot;I feel calm&quot;&lt;/span&gt;)
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;(results[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;][&lt;span class=&quot;v&quot;&gt;'label'&lt;/span&gt;], results[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;][&lt;span class=&quot;v&quot;&gt;'score'&lt;/span&gt;])  &lt;span class=&quot;c&quot;&gt;# POSITIVE 0.8280&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;code&gt;score&lt;/code&gt;는 &lt;strong&gt;softmax&lt;/strong&gt;를 통과한 확률값(모든 값 합=1)이라 &quot;확신도&quot;로 읽을 수 있다. 단, 기본 모델은 영어 전용이고 중립·복합 감정은 잘 못 잡는 한계가 있다.&lt;/div&gt;

    &lt;h3&gt;③ 텍스트 생성 (GPT-2 / KoGPT2)&lt;/h3&gt;
    &lt;p&gt;시드 문장을 주면 뒤 단어를 한 토큰씩 예측해 이어붙인다. 한국어는 &lt;code&gt;skt/kogpt2-base-v2&lt;/code&gt;를 썼다.&lt;/p&gt;
    &lt;pre&gt;generator = pipeline(&lt;span class=&quot;v&quot;&gt;&quot;text-generation&quot;&lt;/span&gt;, model=&lt;span class=&quot;v&quot;&gt;&quot;skt/kogpt2-base-v2&quot;&lt;/span&gt;)
results = generator(answer, max_new_tokens=&lt;span class=&quot;v&quot;&gt;50&lt;/span&gt;, num_return_sequences=&lt;span class=&quot;v&quot;&gt;1&lt;/span&gt;, truncation=&lt;span class=&quot;k&quot;&gt;True&lt;/span&gt;)&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;code&gt;max_new_tokens&lt;/code&gt;는 생성 길이 상한(안 주면 무한 생성 위험), &lt;code&gt;truncation=True&lt;/code&gt;는 입력이 모델 최대 길이를 넘으면 잘라준다. (1 토큰 ≈ 영어 4글자)&lt;/div&gt;

    &lt;h3&gt;④ 요약 (T5-small)&lt;/h3&gt;
    &lt;p&gt;긴 영어 단락을 핵심만 뽑아 다시 쓴다. T5는 모든 작업을 &quot;텍스트 → 텍스트&quot;로 통일한 모델이다.&lt;/p&gt;
    &lt;pre&gt;summarizer = pipeline(&lt;span class=&quot;v&quot;&gt;&quot;summarization&quot;&lt;/span&gt;, model=&lt;span class=&quot;v&quot;&gt;&quot;t5-small&quot;&lt;/span&gt;)
summary = summarizer(text, min_length=&lt;span class=&quot;v&quot;&gt;20&lt;/span&gt;, max_length=&lt;span class=&quot;v&quot;&gt;60&lt;/span&gt;, do_sample=&lt;span class=&quot;k&quot;&gt;False&lt;/span&gt;)
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;(summary[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;][&lt;span class=&quot;v&quot;&gt;'summary_text'&lt;/span&gt;])&lt;/pre&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;Task Prefix&lt;/strong&gt; — T5는 &lt;code&gt;&quot;summarize: 원문&quot;&lt;/code&gt;·&lt;code&gt;&quot;translate English to Korean: ...&quot;&lt;/code&gt;처럼 앞머리(접두사)로 작업을 지시받게 학습됐다. 한 모델로 요약·번역·분류가 다 되는 비결. &lt;code&gt;pipeline(&quot;summarization&quot;)&lt;/code&gt;이 &lt;code&gt;&quot;summarize: &quot;&lt;/code&gt;를 자동으로 붙여준다. &lt;code&gt;do_sample=False&lt;/code&gt;는 매번 같은 결과(결정적 생성).&lt;/div&gt;

    &lt;h3&gt;⑤ 요약 + 한국어 번역&lt;/h3&gt;
    &lt;p&gt;T5는 영어만 잘하니, 요약(영어) 결과를 &lt;code&gt;deep_translator&lt;/code&gt;의 GoogleTranslator로 한국어로 옮겨 한계를 보완했다(API 키 불필요).&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; deep_translator &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; GoogleTranslator

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;translate_ko&lt;/span&gt;(sentence):
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; GoogleTranslator(source=&lt;span class=&quot;v&quot;&gt;'en'&lt;/span&gt;, target=&lt;span class=&quot;v&quot;&gt;'ko'&lt;/span&gt;).translate(sentence)

ko_sum = translate_ko(sum_text)   &lt;span class=&quot;c&quot;&gt;# 영어 요약 → 한국어&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;파이프라인 흐름: &lt;strong&gt;원문(영) → T5 요약 → 요약(영) → GoogleTranslator → 요약(한)&lt;/strong&gt;. 무료·키 불필요·다국어라 편했지만, 인터넷이 필요하고 호출이 많으면 차단될 수 있다.&lt;/div&gt;

    &lt;h2&gt;  OCR — 이미지 속 글자 읽기&lt;/h2&gt;

    &lt;p&gt;&lt;strong&gt;OCR&lt;/strong&gt;(광학 문자 인식)은 사진 속 글자를 읽어내는 기술. 구글 &lt;strong&gt;Tesseract&lt;/strong&gt; 엔진을 파이썬 &lt;code&gt;pytesseract&lt;/code&gt;로 감싸 사용한다.&lt;/p&gt;

    &lt;h3&gt;① OCR 개요 &amp; 설치&lt;/h3&gt;
    &lt;p&gt;&lt;strong&gt;설치 순서&lt;/strong&gt; — ① Tesseract 실행 파일 설치 → ② Python 라이브러리 설치 → ③ 실행 파일 경로 지정&lt;/p&gt;

    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# ① Tesseract-OCR 설치 (Windows)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 공식 설치 경로: https://github.com/UB-Mannheim/tesseract/wiki&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 설치 후 기본 경로: C:/Program Files/Tesseract-OCR/tesseract.exe&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# (한국어 인식은 설치 시 Korean 언어팩 체크)&lt;/span&gt;&lt;/pre&gt;

    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# ② Python 라이브러리 설치&lt;/span&gt;
pip install pytesseract pillow&lt;/pre&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;code&gt;pytesseract&lt;/code&gt; — 파이썬에서 Tesseract를 부르는 통역사. &amp;nbsp;&lt;code&gt;PIL(pillow)&lt;/code&gt; — 사진 파일을 여닫는 파이썬 이미지 도구.&lt;/div&gt;

    &lt;h3&gt;② 이미지 텍스트 인식 (v18_01_ocr_img.py)&lt;/h3&gt;
    &lt;p&gt;PIL로 사진을 열고 &lt;code&gt;image_to_string&lt;/code&gt;으로 글자를 뽑아내는 4단계 흐름이다.&lt;/p&gt;

    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;경로 지정&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;tesseract_cmd로 Tesseract 실행 파일 위치 알려주기&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;이미지 열기&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;Image.open(경로)로 사진 파일 불러오기&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;OCR 수행&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;image_to_string(image, lang)으로 글자 추출&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;결과 출력&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;추출된 문자열 print&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; pytesseract
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; PIL &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; Image
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; os

&lt;span class=&quot;c&quot;&gt;# 1. Tesseract 실행 파일 경로 지정&lt;/span&gt;
pytesseract.pytesseract.tesseract_cmd = &lt;span class=&quot;v&quot;&gt;&quot;C:/Program Files/Tesseract-OCR/tesseract.exe&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 2. 이미지 불러오기&lt;/span&gt;
image = Image.open(&lt;span class=&quot;v&quot;&gt;&quot;image.png&quot;&lt;/span&gt;)

&lt;span class=&quot;c&quot;&gt;# 3. OCR 수행&lt;/span&gt;
results = pytesseract.image_to_string(image, lang=&lt;span class=&quot;v&quot;&gt;'eng'&lt;/span&gt;)

&lt;span class=&quot;c&quot;&gt;# 4. 결과 출력&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;(results)
&lt;span class=&quot;c&quot;&gt;# Optical Character&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Recognition (OCR)&lt;/span&gt;&lt;/pre&gt;

    &lt;div class=&quot;callout blue&quot;&gt;
        &lt;strong&gt;핵심 함수&lt;/strong&gt;&lt;br&gt;
        · &lt;code&gt;pytesseract.tesseract_cmd&lt;/code&gt; — Tesseract 실행 파일 위치 알려주기&lt;br&gt;
        · &lt;code&gt;Image.open(경로)&lt;/code&gt; — PIL로 사진 파일 열기&lt;br&gt;
        · &lt;code&gt;image_to_string(image, lang)&lt;/code&gt; — 사진에서 글자 뽑아 문자열로 반환&lt;br&gt;
        · &lt;code&gt;lang='eng'&lt;/code&gt; — OCR이 쓸 언어 지정 (언어팩 설치 필수)
    &lt;/div&gt;

    &lt;h3&gt;③ lang 파라미터 &amp; 결과 해석&lt;/h3&gt;
    &lt;p&gt;&lt;code&gt;lang&lt;/code&gt;은 OCR에게 읽을 언어를 알려주는 값. &lt;code&gt;+&lt;/code&gt;로 여러 언어를 동시에 인식할 수 있다(예: &lt;code&gt;eng+kor&lt;/code&gt;).&lt;/p&gt;

    &lt;table&gt;
        &lt;tr&gt;&lt;th&gt;lang 값&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;th&gt;예시 코드&lt;/th&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;eng&lt;/code&gt;&lt;/td&gt;&lt;td&gt;영어 (기본값, 별도 설치 없이 사용 가능)&lt;/td&gt;&lt;td&gt;&lt;code&gt;lang='eng'&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;kor&lt;/code&gt;&lt;/td&gt;&lt;td&gt;한국어 (언어팩 별도 설치 필요)&lt;/td&gt;&lt;td&gt;&lt;code&gt;lang='kor'&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;eng+kor&lt;/code&gt;&lt;/td&gt;&lt;td&gt;영어 + 한국어 동시 인식&lt;/td&gt;&lt;td&gt;&lt;code&gt;lang='eng+kor'&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;chi_sim&lt;/code&gt;&lt;/td&gt;&lt;td&gt;중국어 간체&lt;/td&gt;&lt;td&gt;&lt;code&gt;lang='chi_sim'&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;jpn&lt;/code&gt;&lt;/td&gt;&lt;td&gt;일본어&lt;/td&gt;&lt;td&gt;&lt;code&gt;lang='jpn'&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;

    &lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 영어 + 한국어 동시 인식&lt;/span&gt;
results = pytesseract.image_to_string(image, lang=&lt;span class=&quot;v&quot;&gt;'eng+kor'&lt;/span&gt;)
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;(results)&lt;/pre&gt;

    &lt;div class=&quot;callout yellow&quot;&gt;
        · 결과에 줄바꿈(&lt;code&gt;\n&lt;/code&gt;)이 포함됨 → &lt;code&gt;.strip()&lt;/code&gt;으로 공백 제거&lt;br&gt;
        · 사진 해상도가 낮거나 글꼴이 특수하면 인식률 저하 → &lt;strong&gt;전처리&lt;/strong&gt;(흑백 변환·크기 조정)로 개선&lt;br&gt;
        · &lt;code&gt;tesseract_cmd&lt;/code&gt;로 엔진 실행 파일 위치를 먼저 지정해야 함(설치 필수)&lt;br&gt;
        · 더 똑똑한 대안: 딥러닝 기반 &lt;strong&gt;EasyOCR · PaddleOCR&lt;/strong&gt;
    &lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;오늘은 &quot;밑바닥부터 만들기&quot;가 아니라 &lt;em&gt;&quot;잘 만들어진 걸 조립하기&quot;&lt;/em&gt;의 힘을 느낀 하루였다. 공공 API로 실시간 데이터를 끌어오고, HuggingFace·Transformers로 남이 학습해 둔 거대 모델을 몇 줄로 불러 쓰고, OCR로 이미지를 텍스트로 바꾸는 것까지 — 결국 중요한 건 각 도구의 &lt;strong&gt;입력·출력 형태&lt;/strong&gt;(JSON 파싱, messages 리스트, 임베딩 벡터, lang 옵션)를 이해하고 이어 붙이는 감각이었다. &lt;code&gt;.env&lt;/code&gt;로 키를 안전하게 관리하는 습관도 오늘 확실히 챙겼다. 다음엔 이 조각들을 엮어 작은 AI 서비스를 만들어볼 생각이다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ OpenAPI &amp;nbsp;|&amp;nbsp; HuggingFace &amp;nbsp;|&amp;nbsp; Transformers &amp;nbsp;|&amp;nbsp; OCR &amp;nbsp;|&amp;nbsp; NLP &amp;nbsp;|&amp;nbsp; 학습일지&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>huggingface</category>
      <category>NLP</category>
      <category>OCR</category>
      <category>OpenAPI</category>
      <category>Transformers</category>
      <category>학습일지</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/22</guid>
      <comments>https://yundlab.tistory.com/22#entry22comment</comments>
      <pubDate>Wed, 22 Jul 2026 17:28:14 +0900</pubDate>
    </item>
    <item>
      <title>[260721 16일차] YOLO 심화</title>
      <link>https://yundlab.tistory.com/21</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oc9uj/dJMcajwiVBh/WUbVXKrzMHV4TyAhLr06Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oc9uj/dJMcajwiVBh/WUbVXKrzMHV4TyAhLr06Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oc9uj/dJMcajwiVBh/WUbVXKrzMHV4TyAhLr06Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOc9uj%2FdJMcajwiVBh%2FWUbVXKrzMHV4TyAhLr06Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;887&quot; data-filename=&quot;imgi_1_header_74c41465-f820-4a72-8e1e-bdde33db88b3.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;887&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;


&lt;style&gt;
.post-wrap * { box-sizing: border-box; }
.post-wrap ul, .post-wrap ol, .post-wrap li { list-style: none !important; border: none !important; margin: 0; padding: 0; }
.post-wrap {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 24px 80px;
    font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
    color: #333;
    line-height: 1.8;
}
.post-wrap .post-header {
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
    border-left: 5px solid #f97316;
    border-top: none !important;
    border-right: none !important;
    border-bottom: none !important;
    padding-left: 18px;
    padding-top: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 40px;
}
.post-wrap .post-header * { margin-top: 0 !important; }
.post-wrap .tag-badge {
    display: inline-block !important;
    background: #ffedd5;
    color: #f97316;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 10px;
    border-radius: 20px;
    margin-bottom: 10px;
}
.post-wrap .post-header h1 {
    font-size: 26px;
    font-weight: 800;
    line-height: 1.4;
    color: #1a1a2e;
    margin: 0;
}
.post-wrap .subtitle {
    margin-top: 8px;
    color: #666;
    font-size: 14px;
}
.post-wrap h2 {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 48px 0 18px;
    padding-bottom: 10px;
    border-bottom: 2px solid #e9ecef;
}
.post-wrap h3 {
    font-size: 16px !important;
    font-weight: 700 !important;
    color: #2c3e50 !important;
    margin: 28px 0 10px !important;
    padding: 0 !important;
    border: none !important;
    background: none !important;
}
.post-wrap p { margin-bottom: 14px; font-size: 15px; }
.post-wrap .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0;
    border: none !important;
    background: none !important;
}
.post-wrap .summary-card {
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 10px;
    padding: 16px 18px;
    outline: none !important;
    box-shadow: none !important;
}
.post-wrap .summary-card * { border: none !important; outline: none !important; }
.post-wrap .summary-card .card-title {
    font-size: 13px;
    font-weight: 700;
    color: #f97316;
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.post-wrap .summary-card .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.post-wrap .ctag {
    display: inline-block;
    background: #f1f3f9;
    color: #444;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    padding: 2px 8px;
    border-radius: 4px;
    margin: 0;
    line-height: 1.6;
    vertical-align: middle;
}
.post-wrap pre {
    background: #1e2235;
    color: #e8eaf6;
    border-radius: 10px;
    padding: 20px 22px;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.7;
    margin: 14px 0 20px !important;
}
.post-wrap pre .c  { color: #7986cb; }
.post-wrap pre .t  { color: #80cbc4; }
.post-wrap pre .a  { color: #ffcc80; }
.post-wrap pre .v  { color: #a5d6a7; }
.post-wrap pre .k  { color: #f48fb1; }
.post-wrap pre .tx { color: #e8eaf6; }
.post-wrap code {
    font-family: 'Courier New', monospace;
    font-size: 13.5px;
    background: #f1f3f9;
    color: #c0392b;
    padding: 1px 6px;
    border-radius: 4px;
}
.post-wrap .callout { border-radius: 10px; padding: 16px 20px; margin: 18px 0; font-size: 14.5px; }
.post-wrap .callout.blue { background: #fff4ec; border-left: 4px solid #f97316; color: #9a3412; }
.post-wrap .callout.green { background: #fff1e0; border-left: 4px solid #fb923c; color: #b45309; }
.post-wrap .callout.yellow { background: #fef3c7; border-left: 4px solid #f59e0b; color: #92400e; }
.post-wrap table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 14px; }
.post-wrap th { background: #f97316; color: #fff; padding: 10px 14px; text-align: left; font-weight: 700; }
.post-wrap td { padding: 10px 14px; border-bottom: 1px solid #e9ecef; }
.post-wrap tr:nth-child(even) td { background: #fff7ed; }
.post-wrap .cascade-list { margin: 14px 0; padding: 0; border: none; }
.post-wrap .cascade-item {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    padding: 10px 14px !important;
    margin-bottom: 6px !important;
    background: #fff !important;
    border: 1px solid #e9ecef !important;
    border-radius: 8px !important;
    font-size: 14px !important;
}
.post-wrap .cascade-item .cnum {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 24px !important;
    min-width: 24px !important;
    height: 24px;
    border-radius: 50%;
    background: #f97316;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    margin-right: 12px;
    flex-shrink: 0 !important;
}
.post-wrap .cascade-item .cname {
    font-family: 'Courier New', monospace;
    color: #f97316;
    font-weight: 700;
    display: inline-block !important;
    width: 160px !important;
    min-width: 160px !important;
    flex-shrink: 0 !important;
    font-size: 13px;
}
.post-wrap .cascade-item .cdesc { color: #555; flex: 1; display: inline-block !important; }
.post-wrap .closing {
    margin-top: 20px !important;
    padding: 24px 28px;
    background: linear-gradient(135deg, #f9731622, #ea580c22);
    border-radius: 14px;
    border: 1px solid #dee2e6;
}
.post-wrap .closing h3 { margin-top: 0 !important; margin-bottom: 10px; font-size: 17px; }
.post-wrap .closing p { font-size: 14.5px; color: #555; margin-bottom: 6px; }
.post-wrap hr.divider { border: none; border-top: 1px solid #e9ecef; margin: 36px 0; }
&lt;/style&gt;

&lt;div class=&quot;post-wrap&quot;&gt;

    &lt;div class=&quot;post-header&quot;&gt;
        &lt;span class=&quot;tag-badge&quot;&gt;# YOLO 학습일지&lt;/span&gt;
        &lt;h1&gt;[YOLO 심화] 히트맵·보안·카운팅&lt;br&gt;+ SAHI·속도추정·FastSAM&lt;/h1&gt;
        &lt;p class=&quot;subtitle&quot;&gt;2026.07.21 · 학습 범위: YOLO 심화 02~19 (solutions · SAHI · FastSAM · OpenVINO)&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;  오늘 다룬 것들&lt;/h2&gt;

    &lt;p&gt;어제 추적(Tracking)에 이어, 오늘은 YOLO를 감싼 &lt;strong&gt;ultralytics &lt;code&gt;solutions&lt;/code&gt;&lt;/strong&gt; 모듈을 파고들었다. 코드 몇 줄로 히트맵·보안 알람·블러·거리 측정·영역/출입 카운팅·속도 추정 같은 &lt;strong&gt;실전 기능&lt;/strong&gt;이 뚝딱 나온다는 게 놀라웠다. 후반엔 작은 물체를 놓치지 않는 &lt;strong&gt;SAHI&lt;/strong&gt;, 텍스트로 물체를 잘라내는 &lt;strong&gt;FastSAM&lt;/strong&gt;까지 다뤘다. 라이브러리가 얼마나 많은 걸 대신해 주는지 체감한 하루였다.&lt;/p&gt;

    &lt;div class=&quot;summary-grid&quot;&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;실시간 분석&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Heatmap&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;SecurityAlarm&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;ObjectBlurrer&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;거리·영역&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;Distance&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;RegionCounter&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;ObjectCounter&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;작은 물체&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;SAHI&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;슬라이싱&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;속도 · 분할&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;SpeedEstimator&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;FastSAM&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;환경&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;conda py39&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;pip sahi&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;summary-card&quot;&gt;
            &lt;div class=&quot;card-title&quot;&gt;공통 패턴&lt;/div&gt;
            &lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;ctag&quot;&gt;solutions(frame)&lt;/span&gt;&lt;span class=&quot;ctag&quot;&gt;.plot_im&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;h2&gt;  ultralytics.solutions 클래스 모음&lt;/h2&gt;

    &lt;p&gt;&lt;code&gt;from ultralytics import solutions&lt;/code&gt; 하나로 실전 기능들이 클래스로 들어 있다. 오늘은 이 중 여러 개를 직접 써봤다.&lt;/p&gt;
    &lt;div class=&quot;cascade-list&quot;&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;✓&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;Heatmap&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;물체가 자주 나타난 곳을 열지도로 시각화 (오늘)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;✓&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;SecurityAlarm&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;특정 클래스 감지 시 이메일 알림 (오늘)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;✓&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;ObjectBlurrer&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;탐지 영역만 자동 블러 (오늘)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;✓&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;DistanceCalculation&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;두 객체 중심점 사이 픽셀 거리 (오늘)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;✓&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;RegionCounter&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;다각형 영역 안 객체 수 집계 (오늘)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;✓&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;ObjectCounter&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;선을 넘는 IN/OUT 카운팅 (오늘)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;✓&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;SpeedEstimator&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;차량 등 이동 속도(km/h) 추정 (오늘)&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;·&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;AIGym&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;운동 자세·반복 횟수 카운팅&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;·&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;Analytics&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;탐지 결과를 그래프로 통계화&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;·&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;ObjectCropper&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;탐지 영역만 잘라 저장&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;·&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;ParkingManagement&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;주차 구역 점유 관리&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;·&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;QueueManager&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;대기열 인원 관리&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;·&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;TrackZone&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;특정 구역 안에서만 추적&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;·&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;VisionEye&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;시점 기반 객체 매핑&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;cascade-item&quot;&gt;&lt;span class=&quot;cnum&quot;&gt;·&lt;/span&gt;&lt;span class=&quot;cname&quot;&gt;SearchApp / Inference&lt;/span&gt;&lt;span class=&quot;cdesc&quot;&gt;이미지 검색 · Streamlit 추론 UI&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;callout blue&quot;&gt;
        &lt;strong&gt;공통 사용 패턴 — 오늘 계속 나온 형태&lt;/strong&gt; — solutions 객체는 일반 추론(&lt;code&gt;model()&lt;/code&gt;)과 조금 다르다. 프레임을 직접 넣어 호출하고(&lt;code&gt;results = 객체(frame)&lt;/code&gt;), 결과가 그려진 이미지는 &lt;code&gt;results[0].plot()&lt;/code&gt;이 아니라 &lt;strong&gt;&lt;code&gt;results.plot_im&lt;/code&gt;&lt;/strong&gt;으로 꺼낸다. &lt;code&gt;show=True&lt;/code&gt;면 창을 알아서 띄우고, &lt;code&gt;show=False&lt;/code&gt;면 &lt;code&gt;cv2.imshow(..., results.plot_im)&lt;/code&gt;으로 직접 띄운다.
    &lt;/div&gt;

    &lt;h2&gt;  실시간 분석 솔루션&lt;/h2&gt;

    &lt;h3&gt;02 — 히트맵 (Heatmap)&lt;/h3&gt;
    &lt;p&gt;물체가 자주 지나간 곳을 &lt;strong&gt;빨강(자주)~파랑(드묾)&lt;/strong&gt; 열지도로 누적 시각화한다. 매장 동선 분석 등에 쓴다.&lt;/p&gt;
    &lt;pre&gt;heatmap = solutions.Heatmap(
    model=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;,
    colormap=cv2.COLORMAP_MAGMA   &lt;span class=&quot;c&quot;&gt;# 색상 지도(MAGMA=보라~노랑)&lt;/span&gt;
)
results = heatmap(frame)
cv2.imshow(&lt;span class=&quot;v&quot;&gt;&quot;HEATMAP&quot;&lt;/span&gt;, results.plot_im)  &lt;span class=&quot;c&quot;&gt;# 누적 결과&lt;/span&gt;&lt;/pre&gt;

    &lt;h3&gt;03 — 보안 알람 (SecurityAlarm)&lt;/h3&gt;
    &lt;p&gt;특정 클래스(예: 자동차)가 감지되면 &lt;strong&gt;이메일&lt;/strong&gt;을 자동 발송한다. 탐지 → 추적 → 누적 카운트 → SMTP 메일 발송이 내부에서 한 번에 처리된다.&lt;/p&gt;
    &lt;pre&gt;alarm = solutions.SecurityAlarm(
    model=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;, show=&lt;span class=&quot;k&quot;&gt;False&lt;/span&gt;,
    records=&lt;span class=&quot;v&quot;&gt;2&lt;/span&gt;,       &lt;span class=&quot;c&quot;&gt;# N회 감지 시 1회 메일(오탐 방지)&lt;/span&gt;
    classes=[&lt;span class=&quot;v&quot;&gt;2&lt;/span&gt;]      &lt;span class=&quot;c&quot;&gt;# 2 = car&lt;/span&gt;
)
alarm.authenticate(from_email, password, to_email)  &lt;span class=&quot;c&quot;&gt;# SMTP 로그인&lt;/span&gt;
results = alarm(frame)&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;Gmail 앱 비밀번호 필수&lt;/strong&gt; — 2단계 인증을 켠 뒤 &lt;strong&gt;앱 비밀번호(16자리)&lt;/strong&gt;를 발급받아 &lt;code&gt;password&lt;/code&gt;에 넣어야 한다. 일반 로그인 비밀번호로는 SMTP 인증이 막힌다. 실무에선 비밀번호를 코드에 직접 쓰지 말고 환경변수로 분리하는 게 안전하다.&lt;/div&gt;

    &lt;h3&gt;04 — 블러 (ObjectBlurrer)&lt;/h3&gt;
    &lt;p&gt;방송의 모자이크처럼, 탐지된 물체 영역만 골라 자동으로 흐리게 처리한다. 좌표를 직접 계산할 필요가 없다.&lt;/p&gt;
    &lt;pre&gt;blurrer = solutions.ObjectBlurrer(
    model=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;, show=&lt;span class=&quot;k&quot;&gt;False&lt;/span&gt;,
    blur_ratio=&lt;span class=&quot;v&quot;&gt;0.5&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;# 0.1~1.0, 높을수록 더 흐림&lt;/span&gt;
)
results = blurrer(frame)
cv2.imshow(&lt;span class=&quot;v&quot;&gt;&quot;BLUR&quot;&lt;/span&gt;, results.plot_im)&lt;/pre&gt;

    &lt;h2&gt;  거리 · 영역 · 출입&lt;/h2&gt;

    &lt;h3&gt;05 — 거리 계산 (DistanceCalculation)&lt;/h3&gt;
    &lt;p&gt;영상 위에서 &lt;strong&gt;마우스로 객체 2개를 클릭&lt;/strong&gt;하면 두 중심점 사이의 픽셀 거리를 실시간으로 잰다.&lt;/p&gt;
    &lt;pre&gt;distance = solutions.DistanceCalculation(model=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;, show=&lt;span class=&quot;k&quot;&gt;True&lt;/span&gt;)
distance(frame)  &lt;span class=&quot;c&quot;&gt;# 좌클릭 2회로 대상 선택 → 거리 표시&lt;/span&gt;&lt;/pre&gt;

    &lt;h3&gt;06 — 거리 상태 분류 (실습)&lt;/h3&gt;
    &lt;p&gt;거리 값을 꺼내 &lt;strong&gt;신호등처럼 안전/경고/위험&lt;/strong&gt;으로 분류하는 실습. 여기서 solutions 객체를 &lt;code&gt;process()&lt;/code&gt;로 호출하면 결과 객체가 나오고, 거기서 &lt;code&gt;pixels_distance&lt;/code&gt;를 꺼낼 수 있다는 걸 배웠다.&lt;/p&gt;
    &lt;pre&gt;results = distance.process(frame)         &lt;span class=&quot;c&quot;&gt;# 결과 객체 반환&lt;/span&gt;
d = results.pixels_distance               &lt;span class=&quot;c&quot;&gt;# 두 점 사이 픽셀 거리&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; d &lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;or&lt;/span&gt; d == &lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;:
    &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;v&quot;&gt;&quot;입력 안됨&quot;&lt;/span&gt;); &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;# 아직 선택 전&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; d &amp;gt;= &lt;span class=&quot;v&quot;&gt;150&lt;/span&gt;: status = &lt;span class=&quot;v&quot;&gt;&quot;Safe&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; d &amp;gt;= &lt;span class=&quot;v&quot;&gt;100&lt;/span&gt;: status = &lt;span class=&quot;v&quot;&gt;&quot;Warning&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;:          status = &lt;span class=&quot;v&quot;&gt;&quot;Danger&quot;&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;&lt;code&gt;distance(frame)&lt;/code&gt; vs &lt;code&gt;distance.process(frame)&lt;/code&gt;&lt;/strong&gt; — 앞엣것은 화면에 그려 주기만 하고, 뒤엣것은 &lt;strong&gt;결과 객체를 돌려줘&lt;/strong&gt; &lt;code&gt;pixels_distance&lt;/code&gt; 같은 값을 코드로 꺼내 조건 분기에 쓸 수 있다. 상태를 판단해야 하니 &lt;code&gt;process()&lt;/code&gt;를 썼다. 임계값(150·100)은 카메라 위치·줌에 따라 달라져 직접 보정해야 한다.&lt;/div&gt;

    &lt;h3&gt;07 — 영역 카운팅 (RegionCounter)&lt;/h3&gt;
    &lt;p&gt;다각형 &lt;strong&gt;영역(4점)&lt;/strong&gt;을 정의하고, 그 안에 들어온 객체만 센다. 좌표는 화면 해상도에 맞춰야 해서 프레임을 &lt;code&gt;640×480&lt;/code&gt;으로 통일했다.&lt;/p&gt;
    &lt;pre&gt;region_points = { &lt;span class=&quot;v&quot;&gt;&quot;region-01&quot;&lt;/span&gt;: [(&lt;span class=&quot;v&quot;&gt;179&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;114&lt;/span&gt;), (&lt;span class=&quot;v&quot;&gt;173&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;466&lt;/span&gt;), (&lt;span class=&quot;v&quot;&gt;636&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;359&lt;/span&gt;), (&lt;span class=&quot;v&quot;&gt;217&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;133&lt;/span&gt;)] }
region = solutions.RegionCounter(
    model=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;, show=&lt;span class=&quot;k&quot;&gt;False&lt;/span&gt;,
    region=region_points, conf=&lt;span class=&quot;v&quot;&gt;0.4&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;# CCTV 저화질 대응&lt;/span&gt;
)
re_frame = cv2.resize(frame, (&lt;span class=&quot;v&quot;&gt;640&lt;/span&gt;, &lt;span class=&quot;v&quot;&gt;480&lt;/span&gt;))  &lt;span class=&quot;c&quot;&gt;# 좌표계 통일 필수&lt;/span&gt;
results = region(re_frame)&lt;/pre&gt;

    &lt;h3&gt;08 — 마우스로 영역 좌표 추출 (get_region)&lt;/h3&gt;
    &lt;p&gt;영역 좌표를 코드에 하드코딩하지 않고, &lt;strong&gt;화면을 클릭&lt;/strong&gt;해서 좌표를 뽑는 방법. &lt;code&gt;setMouseCallback&lt;/code&gt;에 콜백을 연결해 좌클릭한 (x, y)를 리스트에 모은다.&lt;/p&gt;
    &lt;pre&gt;points = []
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;a&quot;&gt;mouse_callback&lt;/span&gt;(event, x, y, flags, param):
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; event == cv2.EVENT_LBUTTONDOWN:   &lt;span class=&quot;c&quot;&gt;# 좌클릭 감지&lt;/span&gt;
        points.append((x, y))
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;(f&lt;span class=&quot;v&quot;&gt;&quot;클릭 좌표: {x, y}&quot;&lt;/span&gt;)

cv2.namedWindow(&lt;span class=&quot;v&quot;&gt;&quot;GET_X_Y&quot;&lt;/span&gt;, cv2.WINDOW_NORMAL)
cv2.setMouseCallback(&lt;span class=&quot;v&quot;&gt;&quot;GET_X_Y&quot;&lt;/span&gt;, mouse_callback)
&lt;span class=&quot;c&quot;&gt;# 좌상단→좌하단→우하단→우상단 순서로 4점 클릭 → region_points로 복사&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;&lt;strong&gt;콜백 인자 5개는 순서 고정&lt;/strong&gt; — &lt;code&gt;event&lt;/code&gt;(무슨 일)·&lt;code&gt;x, y&lt;/code&gt;(어디)·&lt;code&gt;flags&lt;/code&gt;(같이 누른 키)·&lt;code&gt;param&lt;/code&gt;(밖에서 넘긴 값). 안 써도 &lt;strong&gt;자리는 비워두면 안 된다&lt;/strong&gt;(OpenCV 호출 규칙). 그리고 좌표 측정 창과 실제 카운팅 코드의 &lt;code&gt;resize&lt;/code&gt; 크기가 같아야(640×480) 영역이 안 어긋난다.&lt;/div&gt;

    &lt;h3&gt;09 — 출입 카운팅 (ObjectCounter)&lt;/h3&gt;
    &lt;p&gt;화면에 &lt;strong&gt;선(2점)&lt;/strong&gt;을 긋고 그 선을 넘는 사람·차를 세어 IN/OUT을 집계한다. 4점을 주면 RegionCounter처럼, 2점을 주면 선 카운터로 동작한다.&lt;/p&gt;
    &lt;pre&gt;count_points = [(&lt;span class=&quot;v&quot;&gt;231&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;465&lt;/span&gt;), (&lt;span class=&quot;v&quot;&gt;637&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;418&lt;/span&gt;)]  &lt;span class=&quot;c&quot;&gt;# 점 2개 = 선 1개&lt;/span&gt;
counter = solutions.ObjectCounter(
    model=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;, show=&lt;span class=&quot;k&quot;&gt;False&lt;/span&gt;,
    region=count_points
)
results = counter(re_frame)  &lt;span class=&quot;c&quot;&gt;# 탐지+추적+선통과+IN/OUT 한 번에&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;선은 사람 다니는 길에 &lt;strong&gt;직각&lt;/strong&gt;으로 그어야 잘 세고, IN/OUT이 반대로 나오면 두 점의 순서만 바꾸면 된다. 같은 사람은 처음 넘는 순간 &lt;strong&gt;한 번만&lt;/strong&gt; 카운트된다(내부에서 ID로 잠금).&lt;/div&gt;

    &lt;h2&gt;  SAHI · 속도 · FastSAM&lt;/h2&gt;

    &lt;div class=&quot;callout green&quot;&gt;&lt;strong&gt;환경 준비&lt;/strong&gt; — SAHI는 별도 가상환경에서 실습했다. &lt;code&gt;conda activate py39&lt;/code&gt;로 Python 3.9 환경에 들어가 &lt;code&gt;pip install sahi&lt;/code&gt;로 설치. (버전 호환 때문에 전용 환경을 쓰는 게 편했다.)&lt;/div&gt;

    &lt;h3&gt;10 — SAHI 데이터 준비&lt;/h3&gt;
    &lt;p&gt;멀리 있는 작은 차·사람은 일반 YOLO가 잘 못 본다. &lt;strong&gt;SAHI&lt;/strong&gt;로 해결하는데, 먼저 테스트용 이미지를 내려받았다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; sahi.utils.file &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; download_from_url

download_from_url(
    &lt;span class=&quot;v&quot;&gt;&quot;.../demo_data/small-vehicles1.jpeg&quot;&lt;/span&gt;,  &lt;span class=&quot;c&quot;&gt;# 작은 차량&lt;/span&gt;
    &lt;span class=&quot;v&quot;&gt;&quot;demo_data/small-vehicles1.jpeg&quot;&lt;/span&gt;,
)&lt;/pre&gt;

    &lt;h3&gt;11 — SAHI 기본 추론 (비교용)&lt;/h3&gt;
    &lt;p&gt;사진을 &lt;strong&gt;통째로 한 번&lt;/strong&gt; 추론해 탐지 개수를 세어 둔다. 이 숫자를 다음 슬라이싱 결과와 비교하면 SAHI 효과가 한눈에 보인다.&lt;/p&gt;
    &lt;pre&gt;model = YOLO(&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;)
results = model(&lt;span class=&quot;v&quot;&gt;&quot;demo_data/image_1.png&quot;&lt;/span&gt;)
detected = len(results[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;].boxes)   &lt;span class=&quot;c&quot;&gt;# 탐지 수 기억!&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;(f&lt;span class=&quot;v&quot;&gt;&quot;탐지수 : {detected}&quot;&lt;/span&gt;)&lt;/pre&gt;

    &lt;h3&gt;12 — SAHI 슬라이싱 추론&lt;/h3&gt;
    &lt;p&gt;사진을 &lt;strong&gt;작은 타일(200×200)로 잘라&lt;/strong&gt; 각 조각마다 추론한 뒤 결과를 합친다. 큰 그림을 돋보기로 한 칸씩 보는 셈이라 작은 물체 탐지가 확 늘어난다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; sahi.predict &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; get_sliced_prediction
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; sahi &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; AutoDetectionModel

det_model = AutoDetectionModel.from_pretrained(
    model_type=&lt;span class=&quot;v&quot;&gt;&quot;ultralytics&quot;&lt;/span&gt;, model_path=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;,
    confidence_threshold=&lt;span class=&quot;v&quot;&gt;0.4&lt;/span&gt;
)
results = get_sliced_prediction(
    &lt;span class=&quot;v&quot;&gt;&quot;demo_data/image_1.png&quot;&lt;/span&gt;, det_model,
    slice_width=&lt;span class=&quot;v&quot;&gt;200&lt;/span&gt;, slice_height=&lt;span class=&quot;v&quot;&gt;200&lt;/span&gt;,      &lt;span class=&quot;c&quot;&gt;# 타일 크기&lt;/span&gt;
    overlap_width_ratio=&lt;span class=&quot;v&quot;&gt;0.1&lt;/span&gt;, overlap_height_ratio=&lt;span class=&quot;v&quot;&gt;0.1&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# 경계 10% 겹침&lt;/span&gt;
)
results.export_visuals(export_dir=&lt;span class=&quot;v&quot;&gt;&quot;sahi/&quot;&lt;/span&gt;)&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;타일을 작게 할수록 더 작은 물체를 잡지만 타일 수가 늘어 처리 시간이 증가한다(트레이드오프). 경계에 걸친 물체를 놓치지 않도록 &lt;code&gt;overlap&lt;/code&gt;을 10~20% 준다.&lt;/div&gt;

    &lt;h3&gt;13 — 속도 추정 (SpeedEstimator)&lt;/h3&gt;
    &lt;p&gt;실시간 CCTV에서 차량 속도를 &lt;strong&gt;km/h&lt;/strong&gt;로 추정한다. 핵심은 픽셀을 실제 거리로 바꾸는 &lt;code&gt;meter_per_pixel&lt;/code&gt; 보정값.&lt;/p&gt;
    &lt;pre&gt;speed = solutions.SpeedEstimator(
    model=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;, show=&lt;span class=&quot;k&quot;&gt;False&lt;/span&gt;,
    classes=[&lt;span class=&quot;v&quot;&gt;2&lt;/span&gt;],            &lt;span class=&quot;c&quot;&gt;# car만&lt;/span&gt;
    max_speed=&lt;span class=&quot;v&quot;&gt;120&lt;/span&gt;,          &lt;span class=&quot;c&quot;&gt;# 상한 km/h&lt;/span&gt;
    meter_per_pixel=&lt;span class=&quot;v&quot;&gt;0.5&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;# 1픽셀 = 0.5m (환경별 보정 필수)&lt;/span&gt;
)
results = speed(frame)
cv2.imshow(&lt;span class=&quot;v&quot;&gt;&quot;Speed&quot;&lt;/span&gt;, results.plot_im)&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;code&gt;meter_per_pixel&lt;/code&gt;은 화면 1픽셀이 실제 몇 m인지의 환산 계수다. 도로 점선 한 칸(3m)이 화면에서 6픽셀이면 &lt;code&gt;3 ÷ 6 = 0.5&lt;/code&gt;. 카메라 높이·각도·렌즈마다 달라서 &lt;strong&gt;직접 측정&lt;/strong&gt;해야 하고, 원근 왜곡 때문에 도로 면 근처에서 재야 정확하다.&lt;/div&gt;

    &lt;h3&gt;15 — FastSAM (텍스트로 잘라내기)&lt;/h3&gt;
    &lt;p&gt;SAM(Segment Anything)은 학습 없이 모든 물체를 분할하지만 무겁다. &lt;strong&gt;FastSAM&lt;/strong&gt;은 그걸 YOLO 골격으로 갈아 끼워 훨씬 빠르게 만들고, &lt;code&gt;&quot;cat&quot;&lt;/code&gt;처럼 &lt;strong&gt;텍스트 한 단어&lt;/strong&gt;로 원하는 물체만 콕 집어 잘라낸다.&lt;/p&gt;
    &lt;pre&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; ultralytics &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; FastSAM

model = FastSAM(&lt;span class=&quot;v&quot;&gt;&quot;FastSAM-s.pt&quot;&lt;/span&gt;)          &lt;span class=&quot;c&quot;&gt;# s=소형·빠름&lt;/span&gt;
results = model(&lt;span class=&quot;v&quot;&gt;&quot;img_cat.jpg&quot;&lt;/span&gt;, texts=&lt;span class=&quot;v&quot;&gt;&quot;cat&quot;&lt;/span&gt;) &lt;span class=&quot;c&quot;&gt;# &quot;cat&quot;만 분할&lt;/span&gt;
cv2.imwrite(&lt;span class=&quot;v&quot;&gt;&quot;result.jpg&quot;&lt;/span&gt;, results[&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;].plot())&lt;/pre&gt;
    &lt;div class=&quot;callout green&quot;&gt;SAM은 &quot;모든 물체&quot;를 다 자르고, FastSAM은 &quot;말로 시킨 것만&quot; 빠르게 자른다. (실습 땐 인증서 오류가 나서 &lt;code&gt;ssl&lt;/code&gt;·&lt;code&gt;CA_BUNDLE&lt;/code&gt; 관련 설정으로 다운로드를 우회했다.)&lt;/div&gt;

    &lt;h2&gt;⚡ 모델 최적화 · 웹 UI&lt;/h2&gt;

    &lt;h3&gt;16 — OpenVINO 변환&lt;/h3&gt;
    &lt;p&gt;&lt;strong&gt;OpenVINO&lt;/strong&gt;는 Intel이 만든 CPU 전용 부스터. PyTorch &lt;code&gt;.pt&lt;/code&gt; 모델을 변환하면 GPU 없이 CPU만으로도 &lt;strong&gt;2~3배 빠른 추론&lt;/strong&gt;이 가능하다.&lt;/p&gt;
    &lt;pre&gt;model = YOLO(&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;)
model.export(format=&lt;span class=&quot;v&quot;&gt;&quot;openvino&quot;&lt;/span&gt;)   &lt;span class=&quot;c&quot;&gt;# → yolo11n_openvino_model/ 폴더 생성&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;&lt;code&gt;format&lt;/code&gt;은 &lt;code&gt;&quot;openvino&quot;&lt;/code&gt; 외에 &lt;code&gt;&quot;onnx&quot;&lt;/code&gt;·&lt;code&gt;&quot;tensorrt&quot;&lt;/code&gt; 등도 된다. GPU가 없는 노트북에서 빠르게 돌리고 싶을 때 변환이 유용하다.&lt;/div&gt;

    &lt;h3&gt;17 — PT vs OpenVINO FPS 비교&lt;/h3&gt;
    &lt;p&gt;&lt;strong&gt;FPS&lt;/strong&gt;(초당 프레임 수)로 원본과 변환본의 속도를 겨뤄봤다. 한 프레임 추론에 걸린 시간을 재서 1초로 나누면 FPS가 나온다.&lt;/p&gt;
    &lt;pre&gt;start = time.time()
results = model(frame, verbose=&lt;span class=&quot;k&quot;&gt;False&lt;/span&gt;)
fps = &lt;span class=&quot;v&quot;&gt;1&lt;/span&gt; / (time.time() - start)   &lt;span class=&quot;c&quot;&gt;# 1초 ÷ 1장 걸린 시간&lt;/span&gt;
cv2.putText(annotated, f&lt;span class=&quot;v&quot;&gt;&quot;{fps:.1f} FPS&quot;&lt;/span&gt;, (&lt;span class=&quot;v&quot;&gt;10&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;30&lt;/span&gt;), cv2.FONT_HERSHEY_SIMPLEX, &lt;span class=&quot;v&quot;&gt;1&lt;/span&gt;, (&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;255&lt;/span&gt;,&lt;span class=&quot;v&quot;&gt;0&lt;/span&gt;), &lt;span class=&quot;v&quot;&gt;2&lt;/span&gt;)&lt;/pre&gt;
    &lt;div class=&quot;callout yellow&quot;&gt;모델만 &lt;code&gt;yolo11n.pt&lt;/code&gt; ↔ &lt;code&gt;yolo11n_openvino_model/&lt;/code&gt;로 바꿔 같은 코드로 비교한다. CPU 환경일수록 OpenVINO 효과가 커서 보통 &lt;strong&gt;2~3배&lt;/strong&gt; 빨라진다. &lt;code&gt;verbose=False&lt;/code&gt;는 콘솔 로그를 꺼 측정을 깔끔하게 하려는 용도.&lt;/div&gt;

    &lt;h3&gt;18 — 이미지 검색 (SearchApp)&lt;/h3&gt;
    &lt;p&gt;&quot;&lt;strong&gt;내 사진첩 전용 구글&lt;/strong&gt;&quot;. 폴더를 주면 &lt;code&gt;&quot;cat&quot;&lt;/code&gt; 같은 단어로 비슷한 이미지를 찾아주는 웹앱이다. 내부에 CLIP 임베딩 + FAISS 인덱스가 들어 있어 &quot;비슷한 그림&quot; 찾기가 된다.&lt;/p&gt;
    &lt;pre&gt;app = solutions.SearchApp(
    data=&lt;span class=&quot;v&quot;&gt;&quot;demo_data&quot;&lt;/span&gt;,   &lt;span class=&quot;c&quot;&gt;# 검색 대상 이미지 폴더&lt;/span&gt;
    device=&lt;span class=&quot;v&quot;&gt;&quot;cpu&quot;&lt;/span&gt;          &lt;span class=&quot;c&quot;&gt;# GPU면 &quot;cuda&quot;&lt;/span&gt;
)
app.run(debug=&lt;span class=&quot;k&quot;&gt;False&lt;/span&gt;)     &lt;span class=&quot;c&quot;&gt;# 브라우저에서 자동 오픈&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout green&quot;&gt;Flask 앱이라 &lt;code&gt;python 파일.py&lt;/code&gt;로 실행한다(&lt;code&gt;streamlit run&lt;/code&gt; 쓰면 오류). 여기서도 HuggingFace 모델 다운로드 인증서 문제로 &lt;code&gt;ssl&lt;/code&gt;·&lt;code&gt;CA_BUNDLE&lt;/code&gt;을 우회 설정했다.&lt;/div&gt;

    &lt;h3&gt;19 — Streamlit 추론 UI&lt;/h3&gt;
    &lt;p&gt;코드 &lt;strong&gt;두 줄&lt;/strong&gt;로 뜨는 YOLO 대시보드. 모델 선택·웹캠 입력·결과 표시를 &lt;code&gt;solutions.Inference&lt;/code&gt;가 알아서 그려준다.&lt;/p&gt;
    &lt;pre&gt;inf = solutions.Inference(model=&lt;span class=&quot;v&quot;&gt;&quot;yolo11n.pt&quot;&lt;/span&gt;)
inf.inference()   &lt;span class=&quot;c&quot;&gt;# 실행: streamlit run 파일.py&lt;/span&gt;&lt;/pre&gt;
    &lt;div class=&quot;callout blue&quot;&gt;STEP 18 SearchApp이 &quot;&lt;strong&gt;이미지 검색&lt;/strong&gt;&quot;이라면, Inference는 &quot;&lt;strong&gt;실시간 객체 탐지 UI&lt;/strong&gt;&quot;. 실행 명령이 &lt;code&gt;python&lt;/code&gt;이 아니라 &lt;code&gt;streamlit run&lt;/code&gt;이라는 점만 주의하면 된다.&lt;/div&gt;

    &lt;div class=&quot;closing&quot;&gt;
        &lt;h3&gt;✍️ 마치며&lt;/h3&gt;
        &lt;p&gt;어제까지는 &quot;YOLO로 박스 그리기&quot;였는데, 오늘 &lt;code&gt;solutions&lt;/code&gt;를 보니 히트맵·알람·카운팅·속도까지 &lt;em&gt;실제 서비스에 쓸 법한 기능&lt;/em&gt;이 이미 클래스로 다 만들어져 있었다. 결국 중요한 건 각 클래스가 어떤 입력(영역 좌표·이메일·보정값)을 필요로 하는지, 그리고 결과를 &lt;code&gt;results.plot_im&lt;/code&gt;·&lt;code&gt;process()&lt;/code&gt;로 어떻게 꺼내 쓰는지를 아는 것이었다. SAHI의 &quot;잘라서 보기&quot;나 FastSAM의 &quot;말로 자르기&quot;처럼, 문제를 바라보는 관점 자체가 새로워서 재미있었다. 마지막엔 OpenVINO로 속도를 끌어올리고 SearchApp·Streamlit으로 웹 UI까지 붙여봤다. 다음엔 SAM 본체와 직접 모은 데이터로 커스텀 학습을 이어서 해볼 예정이다.&lt;/p&gt;
        &lt;p style=&quot;margin-top:16px; font-size:13px; color:#888;&quot;&gt; ️ YOLO &amp;nbsp;|&amp;nbsp; solutions &amp;nbsp;|&amp;nbsp; SAHI &amp;nbsp;|&amp;nbsp; OpenCV &amp;nbsp;|&amp;nbsp; AI &amp;nbsp;|&amp;nbsp; 학습일지&lt;/p&gt;
    &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI 기반 프론트엔드 웹개발자 양성과정</category>
      <category>Ai</category>
      <category>OpenCV</category>
      <category>SAHI</category>
      <category>Solutions</category>
      <category>yolo</category>
      <category>학습일지</category>
      <author>yundlab</author>
      <guid isPermaLink="true">https://yundlab.tistory.com/21</guid>
      <comments>https://yundlab.tistory.com/21#entry21comment</comments>
      <pubDate>Wed, 22 Jul 2026 16:46:26 +0900</pubDate>
    </item>
  </channel>
</rss>