<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>스튜디오 폴라리스 공방</title>
    <link>https://polaris-workshop.tistory.com/</link>
    <description>AI로 뭔가를 만듭니다</description>
    <language>ko</language>
    <pubDate>Sat, 18 Jul 2026 17:39:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>스튜디오 폴라리스</managingEditor>
    <item>
      <title>AI로 앱 만들기 #4 - 백엔드 설계하기</title>
      <link>https://polaris-workshop.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이제 백엔드 API를 설계합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드는 눈에 보이지 않는 연산을 담당하는 영역으로, 입력한 데이터를 계산하거나 분류하거나 처리하여 원하는 형태로 출력하는 기능을 말합니다. 예를 들어, 내가 10개의 이미지 파일을 넣었을 때 이 이미지를 그 생성 시간을 기준으로 하여 가장 빠른 순서대로 3, 3, 4개씩 그룹으로 만들어 분류하는 작업이 백엔드가 담당하는 작업입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 백엔드 작업은 (대다수의 작업들이 마찬가지이지만) 크게 보자면 입력과 출력으로 나눠볼 수 있습니다. 입력받은 정보를 연산/가공하여 사용할 수 있는 형태로 전달(출력)해줍니다. 이렇게 백엔드 처리와 입력과 출력을 반복하며 데이터를 주고받기 위한 통로 혹은 인터페이스가 바로 API(Application Programming Interface)입니다. 이 API를 활용하여 각기 다른 개발 언어, 기기 등에서 백엔드에서 만들어진 연산 처리를 사용할 수 있게 해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJEX9w/btsNuBYQaie/3kZC3jhc7kqzS3FDJYUrg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJEX9w/btsNuBYQaie/3kZC3jhc7kqzS3FDJYUrg1/img.png&quot; data-alt=&quot;백엔드의 구조(Created by ChatGPT)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJEX9w/btsNuBYQaie/3kZC3jhc7kqzS3FDJYUrg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJEX9w%2FbtsNuBYQaie%2F3kZC3jhc7kqzS3FDJYUrg1%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;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;백엔드의 구조(Created by ChatGPT)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[백엔드 설계 요청 프롬프트(영어)]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745245681022&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Design the backend that needs to be created for the serverless API endpoint and minimal backend in this service.
And tell us what functions or what content the files created by this should contain, and explain how they communicate with each other.
Make it possible to diagram the communication order and content of each other's communication in Mermaid.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[백엔드 설계 요청 프롬프트(한글)]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745245752078&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;서버리스 API 엔드포인트 및 이 서비스에서 필요한 최소한의 백엔드를 설계해주세요.
이로 인해 생성되는 파일들이 포함해야 할 기능이나 콘텐츠를 알려주고, 서로 어떻게 통신하는지 설명해 주세요.
Mermaid를 사용하여 각 통신 간의 통신 순서와 내용을 다이어그램으로 표현할 수 있도록 설계해 주세요.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서도 말했지만, 기본적으로 프롬프트는 영어로 작성합니다. 한글 프롬프트는 어떤 요청을 보냈는지 내용 파악을 위해 참고용으로 봐 주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면, 아래와 같은 회신을 보내줍니다(답변이 길어 접습니다)&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Below is a &lt;b&gt;conceptual design&lt;/b&gt; of the backend for this Trip Track service, focusing on &lt;b&gt;serverless API endpoints&lt;/b&gt; and a &lt;b&gt;minimal backend&lt;/b&gt; structure using Next.js (or a similar Node.js framework). We&amp;rsquo;ll also walk through &lt;b&gt;what each file or function might do&lt;/b&gt;, how they &lt;b&gt;communicate&lt;/b&gt; with one another, and provide a &lt;b&gt;Mermaid sequence diagram&lt;/b&gt; illustrating the request/response flow.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Overview of the Serverless Backend&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Since you&amp;rsquo;re using Next.js, the simplest approach is to define &lt;b&gt;API routes&lt;/b&gt; in the /pages/api/ or /app/api/ directory (depending on your Next.js version). Each route handles a specific piece of functionality:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Photo Management&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Storing references to uploaded files, associating them with user/trips/groups.&lt;/li&gt;
&lt;li&gt;Possibly receiving direct uploads if not handled fully on the client.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Trip/Group Management&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Creating a new trip, editing or deleting a trip.&lt;/li&gt;
&lt;li&gt;Creating groups, updating group info (place name, review, rating).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Logs (AI Summaries)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Generating an AI travel log by calling the OpenAI API.&lt;/li&gt;
&lt;li&gt;Retrieving/saving the edited log text.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sharing&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Setting a trip&amp;rsquo;s is_public = true and generating a share_id.&lt;/li&gt;
&lt;li&gt;Providing a read-only link to the final travel log.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Depending on your needs, you might combine or split these routes further. For example, you might have a single saveRecord route that updates multiple tables at once, or you might have more granular endpoints (e.g. /api/trips, /api/groups, /api/photos, /api/logs).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;High-Level Responsibilities&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Serverless Functions&lt;/b&gt; (API routes):
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Parse Request&lt;/b&gt;: Validate input, check authentication (if any).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Call Database&lt;/b&gt; (Supabase) to read/write data.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Optionally&lt;/b&gt; call the &lt;b&gt;OpenAI API&lt;/b&gt; to generate text or handle errors.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Return&lt;/b&gt; JSON responses (status, error messages, or data).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Database Operations&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Insert or update records in trips, groups, photos, logs.&lt;/li&gt;
&lt;li&gt;Retrieve existing records for display or editing.&lt;/li&gt;
&lt;li&gt;Enforce RLS policies so only the trip owner can modify, except for public read.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Example File/Function Structure&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Below is a &lt;b&gt;minimal&lt;/b&gt; but structured approach for Next.js API routes. The file paths assume Next.js 13+ with the &lt;b&gt;/app&lt;/b&gt; directory, but you can adjust to /pages/api in older versions.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;app/
 ├─ api/
 │   ├─ trips/
 │   │    ├─ route.ts         (CRUD for trips)
 │   ├─ groups/
 │   │    ├─ route.ts         (CRUD for groups)
 │   ├─ photos/
 │   │    ├─ route.ts         (CRUD for photos)
 │   ├─ logs/
 │   │    ├─ route.ts         (CRUD for logs - fetch or save the AI-generated text)
 │   ├─ generateLog/
 │   │    ├─ route.ts         (Calls OpenAI to generate text)
 │   ├─ share/
 │   │    ├─ route.ts         (Sets is_public and share_id, returns share link)
 │   └─ auth/ (optional)
 │        ├─ route.ts         (Login, logout, user session management)
 └─ ...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Note&lt;/b&gt;: You might also place all routes in a single [...slug].ts file if you prefer a smaller API surface. The following sections describe what goes in each file.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/api/trips/route.ts&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Functions&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /api/trips: Create a new trip.&lt;/li&gt;
&lt;li&gt;GET /api/trips: (Optionally) list user&amp;rsquo;s trips.&lt;/li&gt;
&lt;li&gt;PATCH /api/trips/:id: Update trip info (title, etc.).&lt;/li&gt;
&lt;li&gt;DELETE /api/trips/:id: Delete a trip.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;What it does&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Checks user authorization (if user_id = auth.uid() for the trip) or uses RLS.&lt;/li&gt;
&lt;li&gt;Inserts, selects, updates, or deletes rows in the trips table.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/api/groups/route.ts&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Functions&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /api/groups: Create new groups (e.g., after grouping logic is done client-side).&lt;/li&gt;
&lt;li&gt;PATCH /api/groups/:id: Update place name, rating, etc.&lt;/li&gt;
&lt;li&gt;DELETE /api/groups/:id: Delete a group.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;What it does&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Applies RLS to ensure user can only manipulate groups belonging to their trip.&lt;/li&gt;
&lt;li&gt;Possibly updates earliest_time_stamp if photos in the group change.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/api/photos/route.ts&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Functions&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /api/photos: Insert references to newly uploaded photos.&lt;/li&gt;
&lt;li&gt;PATCH /api/photos/:id: Update photo data (e.g., if location changed).&lt;/li&gt;
&lt;li&gt;DELETE /api/photos/:id: Remove photo references.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;What it does&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Saves the image_url in the DB, plus metadata like time_stamp, latitude, longitude.&lt;/li&gt;
&lt;li&gt;Might rely on &lt;b&gt;Supabase Storage&lt;/b&gt; for the actual file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/api/logs/route.ts&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Functions&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET /api/logs/:groupId: Retrieve the AI-generated or edited text for a group.&lt;/li&gt;
&lt;li&gt;PATCH /api/logs/:groupId: Save user-edited text.&lt;/li&gt;
&lt;li&gt;POST /api/logs: Possibly create an empty log row.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;What it does&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manages the logs table.&lt;/li&gt;
&lt;li&gt;RLS ensures only the trip owner can create/edit logs for a group.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/api/generateLog/route.ts&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Functions&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /api/generateLog:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Receives JSON with group data (places, reviews, etc.).&lt;/li&gt;
&lt;li&gt;Calls &lt;b&gt;OpenAI&lt;/b&gt; to generate a summary.&lt;/li&gt;
&lt;li&gt;Returns the text to the client.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;What it does&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Creates a prompt from the user&amp;rsquo;s group data.&lt;/li&gt;
&lt;li&gt;Calls openai.createChatCompletion(...) or openai.createCompletion(...) with the relevant model.&lt;/li&gt;
&lt;li&gt;Handles errors or token limits.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/api/share/route.ts&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Functions&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /api/share: Sets the trip&amp;rsquo;s is_public = true and creates a unique share_id. Returns the link.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;What it does&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;If the user is the trip owner, updates the trips table (is_public, share_id).&lt;/li&gt;
&lt;li&gt;Returns a shareable URL like /share/abcdef.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/api/auth/route.ts (Optional)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Functions&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /api/auth/login, POST /api/auth/logout, etc.&lt;/li&gt;
&lt;li&gt;If using Supabase Auth, you may not need these routes. You can rely on Supabase&amp;rsquo;s built-in auth.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Communication Flow (Mermaid Diagram)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Below is a &lt;b&gt;sequence diagram&lt;/b&gt; showing the typical flow of requests among:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;The &lt;b&gt;Browser&lt;/b&gt; (frontend pages),&lt;/li&gt;
&lt;li&gt;The &lt;b&gt;API Routes&lt;/b&gt; (minimal backend/serverless functions),&lt;/li&gt;
&lt;li&gt;The &lt;b&gt;Database&lt;/b&gt; (Supabase),&lt;/li&gt;
&lt;li&gt;The &lt;b&gt;OpenAI API&lt;/b&gt; (for generating AI text).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sequenceDiagram
    participant U as User Browser (Frontend)
    participant S as Serverless API (Next.js)
    participant DB as Supabase DB
    participant AI as OpenAI API

    U-&amp;gt;&amp;gt;S: 1. POST /api/photos (metadata)
    S-&amp;gt;&amp;gt;DB: Insert photo records (RLS enforced)

    U-&amp;gt;&amp;gt;S: 2. POST /api/groups (after grouping)
    S-&amp;gt;&amp;gt;DB: Insert new group rows

    U-&amp;gt;&amp;gt;S: 3. PATCH /api/groups/:id (update place name, rating)
    S-&amp;gt;&amp;gt;DB: Update group data

    U-&amp;gt;&amp;gt;S: 4. POST /api/generateLog
    S-&amp;gt;&amp;gt;DB: (Optional) fetch group info
    S-&amp;gt;&amp;gt;AI: Call OpenAI GPT with group data
    AI--&amp;gt;&amp;gt;S: Return generated text
    S--&amp;gt;&amp;gt;U: Send AI-generated text response

    U-&amp;gt;&amp;gt;S: 5. PATCH /api/logs/:groupId (save edited text)
    S-&amp;gt;&amp;gt;DB: Update logs table with final text

    U-&amp;gt;&amp;gt;S: 6. POST /api/share (make trip public)
    S-&amp;gt;&amp;gt;DB: Update trips (is_public=true, share_id=...)
    S--&amp;gt;&amp;gt;U: Return share link (e.g. /share/abc123)

    U-&amp;gt;&amp;gt;DB: 7. (Public link access) GET /share/abc123
    DB--&amp;gt;&amp;gt;U: Return trip data if is_public=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Explanation of Diagram Steps&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Photo Upload&lt;/b&gt;: The user has extracted EXIF data on the client; they send a request to store photo references in the DB.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Group Creation&lt;/b&gt;: The user finalizes grouping on the client and calls an API route to create those groups in the database.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Group Updates&lt;/b&gt;: The user sets place names, reviews, etc. via a PATCH or POST request.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Generate Log&lt;/b&gt;: The client calls /api/generateLog with summarized group data. The serverless function fetches or re-checks the data from Supabase, then calls the OpenAI API, returning the AI-generated text.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Edit &amp;amp; Save&lt;/b&gt;: The user modifies the AI text and saves it to the logs table via a PATCH request.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Share&lt;/b&gt;: The user marks a trip as public (is_public = true) by hitting /api/share. The serverless function sets a share_id, returns the new shareable URL.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;View Shared Page&lt;/b&gt;: Anyone with the share link (e.g., /share/abc123) can fetch trip data from the DB if is_public = true.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;How the Files Communicate&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Frontend Pages&lt;/b&gt; (e.g., upload.tsx, grouping.tsx, group-info.tsx, etc.)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Collect user input and send &lt;b&gt;JSON requests&lt;/b&gt; to the serverless APIs.&lt;/li&gt;
&lt;li&gt;Receive JSON responses (e.g., success/failure, generated text).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Serverless API Routes&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Validate&lt;/b&gt; incoming requests (check auth if needed).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Query/Update&lt;/b&gt; the Supabase database using the official JS client (@supabase/supabase-js) or direct Postgres queries.&lt;/li&gt;
&lt;li&gt;If generating AI text, call the &lt;b&gt;OpenAI API&lt;/b&gt; with the user data.&lt;/li&gt;
&lt;li&gt;Return responses to the frontend (success messages, error codes, data).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Supabase Database&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stores all persistent data (users, trips, groups, photos, logs).&lt;/li&gt;
&lt;li&gt;Enforces row-level security (RLS) to ensure only owners or public readers can see/modify data.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OpenAI API&lt;/b&gt; (for generateLog route)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Receives a prompt with user&amp;rsquo;s trip info (dates, places, reviews).&lt;/li&gt;
&lt;li&gt;Returns a structured text summary.&lt;/li&gt;
&lt;li&gt;The serverless route passes it back to the frontend.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Serverless Approach&lt;/b&gt;: Each &lt;b&gt;API route&lt;/b&gt; is a standalone function that handles a specific domain (trips, groups, photos, logs, AI generation, sharing).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Database Communication&lt;/b&gt;: Use &lt;b&gt;Supabase&lt;/b&gt; for reading/writing, with RLS ensuring data security.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI Calls&lt;/b&gt;: The route that handles text generation calls the OpenAI API, then returns the result to the user.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sharing&lt;/b&gt;: Another route sets a trip to is_public and returns a share link.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Mermaid Sequence Diagram&lt;/b&gt;: Illustrates the overall flow&amp;mdash;frontend calls serverless endpoints, endpoints talk to DB/AI, and everything returns back to the user.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;This design supports a &lt;b&gt;minimal&lt;/b&gt; but &lt;b&gt;scalable&lt;/b&gt; backend, leverages Next.js serverless APIs, and keeps each domain concern (photos, groups, logs, sharing) in its own file or route for maintainability.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 어떤 백엔드 API가 생성되어야 하고, 그것이 각각 어떤 기능을 하며 API로 어떻게 통신하는지를 정의하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용도 txt 파일로 만들어 BACKEND STRUCTURE.txt 라는 이름의 파일로 업로드합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설계 혹은 기획의 단계를 최대한 챗GPT를 사용하여 작업하고, 실제 코딩 작업은 Github Copilot을 사용하여 IDE 환경에서 작업합니다. 다음에는 이 설계를 기반으로 VSCode 환경에서 어떻게 작업하는지 설명해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI로 1인 개발</category>
      <category>1인개발</category>
      <category>AI</category>
      <category>AI개발</category>
      <category>ChatGPT</category>
      <category>COPILOT</category>
      <category>LLM</category>
      <category>바이브코딩</category>
      <author>스튜디오 폴라리스</author>
      <guid isPermaLink="true">https://polaris-workshop.tistory.com/4</guid>
      <comments>https://polaris-workshop.tistory.com/4#entry4comment</comments>
      <pubDate>Mon, 21 Apr 2025 23:33:24 +0900</pubDate>
    </item>
    <item>
      <title>AI로 앱 만들기 #3 - DB 설계하기</title>
      <link>https://polaris-workshop.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이후의 흐름은 작업하는 사람의 취향/습관 등에 따라 다를 수 있지만 저는 기본적으로 &lt;u&gt;DB 설계&lt;/u&gt; &amp;rarr; &lt;u&gt;백엔드 API 설계 및 개발&lt;/u&gt; &amp;rarr; &lt;u&gt;프론트엔드 화면 기획 및 개발&lt;/u&gt; &amp;rarr; &lt;u&gt;프론트엔드-백엔드 API 연결&lt;/u&gt;의 방식을 따라 진행합니다. 뒤에서 어떻게 작동하는지, 원하는대로 백엔드가 기능하고 데이터를 주고받는지를 미리 확인하지 않으면 프론트엔드에서의 유저플로우 등이 깨지는 경우를 자주 봤기 때문입니다. 물론 서로 분업화가 잘 되어있는 개발 조직에서는 다른 방식을 활용하고 있겠지만, 1인 개발의 환경에서는 이러한 방향의 개발이 효과적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RAG 참고용 영문 문서 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터의 프롬프트는 모두 영어로 입력합니다. 그 이유는 대부분의 개발 자료 및 문서가 영어로 작성되어 있기 때문에 영어로 입력/출력하는 것이 효과적/효율적인 결과를 가져오기 때문입니다. 이를 위해서 우선 기초 문서들을 모두 영어로 바꿔줍니다. 문서의 포맷은 그대로 txt로 하고, 아래의 프롬프트로 번역을 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[문서 번역 프롬프트(o1)]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744640143863&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(문서명)을 영어로 만들어서 RAG에서 사용할 수 있도록 txt로 활용할 수 있는 양식으로 바꿔줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 모델은 가급적 추론(reasoning)을 사용하는 모델인 o1, o3-mini-high 등을 사용하도록 합니다. 그 이유는 단순한 확률적 생성이 아닌 지금까지의 맥락과 흐름을 파악하고 출력을 생성하기 때문입니다.(그런데 아마 프로젝트에 RAG를 사용하면 o3 모델은 사용할 수 없는 것으로 알고 있습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성한 것들을 기존에 프로젝트에 입력했던 것과 동일한 파일명으로 교체해 줍니다. 이렇게 되면 프로젝트의 지침은 변경하지 않은 상태로 RAG 파일들만 교체할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DB 설계하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터 본격적으로 DB 설계를 요청합니다. 여기에서 요청할 내용을 우선 한글로 쓰고, 그 내용을 번역기를 통해 영어로 번역하여 전달합니다. 그리고 제대로 결과가 나오는지도 번역기를 통해 확인합니다. 다소 번거롭기는 하지만, 이렇게 작업하는 쪽의 출력물이 더 나은 경우가 많았고 장기적으로 보면 시간을 아낄 수 있었습니다. 저는 DeepL 번역기를 사용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[DB 설계 프롬프트(한국어)]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744640485186&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;먼저, 전체 서비스에 대한 데이터베이스 스키마를 설계해 주세요. 
그런 다음, Supabase를 사용하여 이를 구현하는 방법을 알려주세요. 
또한, 머메이드 언어를 사용하여 각 데이터베이스 간의 관계를 ERD 형식으로 표시합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[DB 설계 프롬프트(영어) (o1)]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744640452082&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;First, design the database schema for the entire service. 
Then, tell me how to implement this with Supabase.
Also, use mermaid to diagram the relationships between each database.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 아래와 같이 답변을 줍니다. 이것을 번역기를 통해 다시 확인해봅니다.(답변이 길어 접었습니다)&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Below is a suggested relational schema for your Trip Track service, along with a Mermaid ER diagram showing how the tables connect in Supabase (Postgres). Following that, you&amp;rsquo;ll see details on how to implement this schema in Supabase&amp;mdash;either via SQL statements in the Supabase SQL editor or via the Supabase dashboard UI.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Proposed Database Schema&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;This schema covers users (if you want multi-user support), trips, grouping of photos, AI-generated travel logs, and the shareable links for each trip. It&amp;rsquo;s structured so that:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Users&lt;/b&gt; have many &lt;b&gt;Trips&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;Each &lt;b&gt;Trip&lt;/b&gt; can have multiple &lt;b&gt;Groups&lt;/b&gt; (one group for each time/location cluster).&lt;/li&gt;
&lt;li&gt;Each &lt;b&gt;Group&lt;/b&gt; can have multiple &lt;b&gt;Photos&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;Each &lt;b&gt;Trip&lt;/b&gt; can optionally have a single record in &lt;b&gt;Travel_Logs&lt;/b&gt; (or multiple if you want versioning).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Schema Overview:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;users&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt; (UUID, primary key)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;email&lt;/b&gt; (text, unique)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;display_name&lt;/b&gt; (text, optional)&lt;/li&gt;
&lt;li&gt;(Any other user-related fields if you need authentication)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;trips&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt; (UUID, primary key)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;user_id&lt;/b&gt; (UUID, references &lt;b&gt;users.id&lt;/b&gt;, on delete cascade)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;title&lt;/b&gt; (text, optional, e.g. &amp;ldquo;My Paris Trip&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;created_at&lt;/b&gt; (timestamp, default now())&lt;/li&gt;
&lt;li&gt;&lt;b&gt;is_public&lt;/b&gt; (boolean, default false)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;share_id&lt;/b&gt; (text, unique, nullable)&lt;br /&gt;Used for generating a shareable URL like /share/abcdef.&lt;/li&gt;
&lt;li&gt;(You could also store a &amp;ldquo;cover_photo_url&amp;rdquo; or &amp;ldquo;start_date&amp;rdquo; etc. if desired)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;groups&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt; (UUID, primary key)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;trip_id&lt;/b&gt; (UUID, references &lt;b&gt;trips.id&lt;/b&gt;, on delete cascade)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;group_name&lt;/b&gt; (text, e.g. &amp;ldquo;Eiffel Tower Morning&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;rating&lt;/b&gt; (integer or numeric, optional)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;review&lt;/b&gt; (text, optional)&lt;/li&gt;
&lt;li&gt;(Timestamps if needed)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;photos&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt; (UUID, primary key)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;group_id&lt;/b&gt; (UUID, references &lt;b&gt;groups.id&lt;/b&gt;, on delete cascade)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;image_url&lt;/b&gt; (text)&lt;br /&gt;Location of the file in Supabase Storage or external URL&lt;/li&gt;
&lt;li&gt;&lt;b&gt;time_stamp&lt;/b&gt; (timestamp, optional)&lt;br /&gt;Extracted from EXIF or fallback to file&amp;rsquo;s lastModified&lt;/li&gt;
&lt;li&gt;&lt;b&gt;latitude&lt;/b&gt; (numeric, optional)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;longitude&lt;/b&gt; (numeric, optional)&lt;/li&gt;
&lt;li&gt;(Any other metadata you need)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;travel_logs&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt; (UUID, primary key)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;trip_id&lt;/b&gt; (UUID, references &lt;b&gt;trips.id&lt;/b&gt;, on delete cascade, unique if 1:1)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ai_generated_text&lt;/b&gt; (text)&lt;br /&gt;The main text returned by AI&lt;/li&gt;
&lt;li&gt;&lt;b&gt;edited_text&lt;/b&gt; (text, optional)&lt;br /&gt;The user&amp;rsquo;s final edited version (or you can store final text only)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;created_at&lt;/b&gt; (timestamp, default now())&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;You can expand or reduce fields as needed. For instance, if you want to store multiple versions of the travel log, you can remove the unique constraint on trip_id in &lt;b&gt;travel_logs&lt;/b&gt;.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Mermaid ER Diagram&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;erDiagram

    USERS ||--|{ TRIPS : &quot;has many&quot;
    TRIPS ||--|{ GROUPS : &quot;has many&quot;
    GROUPS ||--|{ PHOTOS : &quot;has many&quot;
    TRIPS ||--|| TRAVEL_LOGS : &quot;has one (1:1)&quot;

    USERS {
        uuid id PK
        text email
        text display_name
        // ...other fields
    }

    TRIPS {
        uuid id PK
        uuid user_id FK
        text title
        timestamp created_at
        boolean is_public
        text share_id
        // ...other fields
    }

    GROUPS {
        uuid id PK
        uuid trip_id FK
        text group_name
        int rating
        text review
        // ...other fields
    }

    PHOTOS {
        uuid id PK
        uuid group_id FK
        text image_url
        timestamp time_stamp
        numeric latitude
        numeric longitude
        // ...other fields
    }

    TRAVEL_LOGS {
        uuid id PK
        uuid trip_id FK
        text ai_generated_text
        text edited_text
        timestamp created_at
        // ...other fields
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;USERS&lt;/b&gt; &amp;rarr; &lt;b&gt;TRIPS&lt;/b&gt;: One user can have many trips.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TRIPS&lt;/b&gt; &amp;rarr; &lt;b&gt;GROUPS&lt;/b&gt;: Each trip can have multiple groups.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GROUPS&lt;/b&gt; &amp;rarr; &lt;b&gt;PHOTOS&lt;/b&gt;: Each group can have multiple photos.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TRIPS&lt;/b&gt; &amp;rarr; &lt;b&gt;TRAVEL_LOGS&lt;/b&gt;: One-to-one or one-to-many, depending on your design.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Implementing the Schema in Supabase&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Option A: Using the Supabase SQL Editor&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Go to your Supabase project dashboard.&lt;/li&gt;
&lt;li&gt;Select the &lt;b&gt;SQL&lt;/b&gt; section.&lt;/li&gt;
&lt;li&gt;Create each table with DDL statements (see sample code below).&lt;/li&gt;
&lt;li&gt;Adjust or omit columns as needed.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- Enable UUID extension (if not already enabled)
CREATE EXTENSION IF NOT EXISTS &quot;uuid-ossp&quot;;

-- 1. USERS TABLE
CREATE TABLE IF NOT EXISTS users (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  email text UNIQUE NOT NULL,
  display_name text,
  created_at timestamp DEFAULT now()
);

-- 2. TRIPS TABLE
CREATE TABLE IF NOT EXISTS trips (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id uuid REFERENCES users (id) ON DELETE CASCADE,
  title text,
  created_at timestamp DEFAULT now(),
  is_public boolean DEFAULT false,
  share_id text UNIQUE,
  -- You can store additional fields here
  CONSTRAINT fk_user
    FOREIGN KEY (user_id)
    REFERENCES users (id)
    ON DELETE CASCADE
);

-- 3. GROUPS TABLE
CREATE TABLE IF NOT EXISTS groups (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  trip_id uuid NOT NULL REFERENCES trips (id) ON DELETE CASCADE,
  group_name text,
  rating integer,
  review text,
  created_at timestamp DEFAULT now(),
  CONSTRAINT fk_trip
    FOREIGN KEY (trip_id)
    REFERENCES trips (id)
    ON DELETE CASCADE
);

-- 4. PHOTOS TABLE
CREATE TABLE IF NOT EXISTS photos (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  group_id uuid NOT NULL REFERENCES groups (id) ON DELETE CASCADE,
  image_url text NOT NULL,
  time_stamp timestamp,
  latitude numeric,
  longitude numeric,
  created_at timestamp DEFAULT now(),
  CONSTRAINT fk_group
    FOREIGN KEY (group_id)
    REFERENCES groups (id)
    ON DELETE CASCADE
);

-- 5. TRAVEL_LOGS TABLE
CREATE TABLE IF NOT EXISTS travel_logs (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  trip_id uuid NOT NULL REFERENCES trips (id) ON DELETE CASCADE,
  ai_generated_text text,
  edited_text text,
  created_at timestamp DEFAULT now(),
  CONSTRAINT fk_trip
    FOREIGN KEY (trip_id)
    REFERENCES trips (id)
    ON DELETE CASCADE
    -- If you want exactly 1:1, add a UNIQUE constraint on trip_id:
    --, CONSTRAINT unique_trip_id UNIQUE (trip_id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Run each statement in the Supabase SQL editor.&lt;/li&gt;
&lt;li&gt;Verify that each table is created successfully under the &amp;ldquo;Database&amp;rdquo; tab.&lt;/li&gt;
&lt;li&gt;If you need other fields, simply alter the table or add them in the initial creation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Option B: Using the Supabase Table Editor&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Navigate to the &lt;b&gt;Table Editor&lt;/b&gt; in the Supabase dashboard.&lt;/li&gt;
&lt;li&gt;Create each table (users, trips, groups, photos, travel_logs) with the columns listed above.&lt;/li&gt;
&lt;li&gt;For each foreign key, set the reference to the appropriate parent table.&lt;/li&gt;
&lt;li&gt;If you want to enable cascade deletes, set the proper constraint.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Storing Images in Supabase Storage&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Inside your Supabase dashboard, you can create a &lt;b&gt;Storage Bucket&lt;/b&gt; (e.g., &amp;ldquo;travel-photos&amp;rdquo;).&lt;/li&gt;
&lt;li&gt;When a user uploads an image, upload it to the bucket using the Supabase client on the front end:&lt;/li&gt;
&lt;li&gt;const { data, error } = await supabase.storage .from('travel-photos') .upload('folder/photo1.jpg', file)&lt;/li&gt;
&lt;li&gt;Store the resulting public URL or file path in the photos.image_url field.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Accessing the Data from Next.js&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Install the Supabase client:&lt;/li&gt;
&lt;li&gt;npm install @supabase/supabase-js&lt;/li&gt;
&lt;li&gt;Initialize and connect to your Supabase instance (e.g., in a lib/supabaseClient.ts file):&lt;/li&gt;
&lt;li&gt;import { createClient } from '@supabase/supabase-js' const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY export const supabase = createClient(supabaseUrl, supabaseAnonKey)&lt;/li&gt;
&lt;li&gt;In your API routes or serverless functions, you can insert/select data:&lt;/li&gt;
&lt;li&gt;// Example: saving a new trip const { data, error } = await supabase .from('trips') .insert([{ user_id: userId, title: 'My Paris Trip' }]) .single()&lt;/li&gt;
&lt;li&gt;Use RLS (Row Level Security) and policies if you need to secure data per user.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;The above schema implements all the necessary relationships to store user information, trips, photo groups, photos, and the AI-generated travel log.&lt;/li&gt;
&lt;li&gt;Supabase (Postgres) makes it straightforward to set up relational tables, foreign keys, and references.&lt;/li&gt;
&lt;li&gt;You can use the SQL approach directly in the editor or the UI Table Editor to create and manage these tables.&lt;/li&gt;
&lt;li&gt;Make sure to configure your security rules and set up your environment variables for local and deployed environments as needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 DB 구조를 파악하기 위해 ERD를 확인해보면 아래와 같습니다.(일부 의도하던 내용과 결과물이 다소 달라 중간에 구조를 일부 수정한 부분이 있습니다. 이러한 수정 역시 한-영 번역을 통해서 진행했습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;881&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qJIAY/btsNlh7SdM8/zElXSILYVK6MzXsRsDOEVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qJIAY/btsNlh7SdM8/zElXSILYVK6MzXsRsDOEVk/img.png&quot; data-alt=&quot;DB의 ERD 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qJIAY/btsNlh7SdM8/zElXSILYVK6MzXsRsDOEVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqJIAY%2FbtsNlh7SdM8%2FzElXSILYVK6MzXsRsDOEVk%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;457&quot; height=&quot;881&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;881&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DB의 ERD 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 아래 프롬프트로 이렇게 설계한 DB 구조 역시 문서화하여 RAG에 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[DB 구조 문서화 요청 프롬프트(4o or 4.5)]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744640791702&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;I want to store this database structure in RAG and continue using it.
I'm going to create a file in txt format, and I'd like to organize the current database structure so that it's easy to use.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DB 설정하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성한 DB 구조를 기반으로, 세팅해 둔 supabase에 DB 테이블을 생성합니다. supabase 역시 익숙하지 않은 환경이라 아래 프롬프트를 통해 상세 가이드를 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[supabase 세팅 요청 프롬프트(o1)]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744640867390&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Please tell me step by step how to create this database in Supabase.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면, 아래와 같이 supabase에 DB를 세팅하는 가이드를 제공해줍니다. 이를 하나하나 따라가면서 DB를 세팅합니다.(길이가 길어 접음 처리합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Below is a step-by-step guide on how to &lt;b&gt;create this database in Supabase&lt;/b&gt; using two approaches: (A) &lt;b&gt;SQL Editor&lt;/b&gt; and (B) &lt;b&gt;Table Editor&lt;/b&gt; in the Supabase dashboard.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Create a Supabase Project&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Sign in to &lt;a href=&quot;https://app.supabase.com/&quot;&gt;Supabase&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create a new project or open an existing one.&lt;/li&gt;
&lt;li&gt;Make sure you have the right &lt;b&gt;Database Password&lt;/b&gt; and &lt;b&gt;Project API Keys&lt;/b&gt; stored securely.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Approach A: Using the Supabase SQL Editor&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;This method uses a single SQL script to create tables and relationships.&lt;br /&gt;You can copy-paste the script below into the &lt;b&gt;SQL&lt;/b&gt; section of your Supabase dashboard.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. Open the SQL Editor&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;In your Supabase dashboard, open the left menu and click &lt;b&gt;SQL&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;b&gt;New query&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. Run the SQL Script&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Important&lt;/b&gt;: Some of these statements may need the uuid-ossp extension if you haven&amp;rsquo;t already enabled it. You can enable that with:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE EXTENSION IF NOT EXISTS &quot;uuid-ossp&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Then proceed with the rest:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- Create the &quot;users&quot; table
CREATE TABLE IF NOT EXISTS users (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  email text UNIQUE NOT NULL,
  display_name text,
  created_at timestamp DEFAULT now()
);

-- Create the &quot;trips&quot; table
CREATE TABLE IF NOT EXISTS trips (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id uuid REFERENCES users(id) ON DELETE CASCADE,
  title text,
  created_at timestamp DEFAULT now(),
  is_public boolean DEFAULT false,
  share_id text UNIQUE
);

-- Create the &quot;groups&quot; table
CREATE TABLE IF NOT EXISTS groups (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  trip_id uuid NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
  group_name text,
  rating integer,
  review text,
  created_at timestamp DEFAULT now(),
  sort_order integer,
  earliest_time_stamp timestamp,
  representative_location text
);

-- Create the &quot;photos&quot; table
CREATE TABLE IF NOT EXISTS photos (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  group_id uuid NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
  image_url text NOT NULL,
  time_stamp timestamp,
  latitude numeric,
  longitude numeric,
  created_at timestamp DEFAULT now()
);

-- Create the &quot;logs&quot; table
CREATE TABLE IF NOT EXISTS logs (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  group_id uuid NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
  ai_generated_text text,
  edited_text text,
  created_at timestamp DEFAULT now(),
  CONSTRAINT unique_group_id UNIQUE (group_id)
  -- The above constraint makes logs.group_id 1:1 with groups.id
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. Verify the Tables&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;In the Supabase dashboard, navigate to the &lt;b&gt;Table Editor&lt;/b&gt; or &lt;b&gt;Database&lt;/b&gt; section to see if the tables (users, trips, groups, photos, logs) were created.&lt;/li&gt;
&lt;li&gt;Check each table&amp;rsquo;s columns to confirm everything matches your schema.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Approach B: Using the Supabase Table Editor&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;If you prefer a point-and-click interface instead of writing SQL:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;In the Supabase dashboard&lt;/b&gt;, select &lt;b&gt;Table Editor&lt;/b&gt; &amp;rarr; &lt;b&gt;New Table&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create the &amp;ldquo;users&amp;rdquo; table&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Add columns as described:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id (UUID, primary key, default uuid_generate_v4())&lt;/li&gt;
&lt;li&gt;email (text, unique, not null)&lt;/li&gt;
&lt;li&gt;display_name (text, optional)&lt;/li&gt;
&lt;li&gt;created_at (timestamp, default now())&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the table.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create the &amp;ldquo;trips&amp;rdquo; table&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Columns:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id (UUID, primary key)&lt;/li&gt;
&lt;li&gt;user_id (UUID, foreign key referencing users.id, ON DELETE CASCADE)&lt;/li&gt;
&lt;li&gt;title (text, optional)&lt;/li&gt;
&lt;li&gt;created_at (timestamp, default now())&lt;/li&gt;
&lt;li&gt;is_public (boolean, default false)&lt;/li&gt;
&lt;li&gt;share_id (text, unique)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the table.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create the &amp;ldquo;groups&amp;rdquo; table&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Columns:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id (UUID, primary key)&lt;/li&gt;
&lt;li&gt;trip_id (UUID, foreign key referencing trips.id, ON DELETE CASCADE)&lt;/li&gt;
&lt;li&gt;group_name (text)&lt;/li&gt;
&lt;li&gt;rating (integer)&lt;/li&gt;
&lt;li&gt;review (text)&lt;/li&gt;
&lt;li&gt;created_at (timestamp, default now())&lt;/li&gt;
&lt;li&gt;sort_order (integer)&lt;/li&gt;
&lt;li&gt;earliest_time_stamp (timestamp)&lt;/li&gt;
&lt;li&gt;representative_location (text)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the table.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create the &amp;ldquo;photos&amp;rdquo; table&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Columns:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id (UUID, primary key)&lt;/li&gt;
&lt;li&gt;group_id (UUID, foreign key referencing groups.id, ON DELETE CASCADE)&lt;/li&gt;
&lt;li&gt;image_url (text, not null)&lt;/li&gt;
&lt;li&gt;time_stamp (timestamp)&lt;/li&gt;
&lt;li&gt;latitude (numeric)&lt;/li&gt;
&lt;li&gt;longitude (numeric)&lt;/li&gt;
&lt;li&gt;created_at (timestamp, default now())&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the table.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create the &amp;ldquo;logs&amp;rdquo; table&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Columns:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id (UUID, primary key)&lt;/li&gt;
&lt;li&gt;group_id (UUID, foreign key referencing groups.id, ON DELETE CASCADE)&lt;/li&gt;
&lt;li&gt;ai_generated_text (text)&lt;/li&gt;
&lt;li&gt;edited_text (text)&lt;/li&gt;
&lt;li&gt;created_at (timestamp, default now())&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Under &lt;b&gt;Constraints&lt;/b&gt;, add a &lt;b&gt;Unique&lt;/b&gt; constraint for group_id if you want the 1:1 relationship.&lt;/li&gt;
&lt;li&gt;Save the table.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;After each table is created, you can confirm the relationships by reviewing the foreign key references in the &lt;b&gt;Table Editor&lt;/b&gt; or by checking the &amp;ldquo;Database&amp;rdquo; &amp;rarr; &amp;ldquo;Relations&amp;rdquo; page.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;(Optional) Setting Up Row Level Security (RLS)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;By default, &lt;b&gt;Supabase&lt;/b&gt; uses Row Level Security. If you want to control which users can access or modify which rows, you can create &lt;b&gt;Policies&lt;/b&gt; on each table.&lt;/li&gt;
&lt;li&gt;For example, you might add a policy on trips so that only the user who owns a trip can read or write it.&lt;/li&gt;
&lt;li&gt;CREATE POLICY &quot;Allow users to read their trips&quot; ON trips FOR SELECT TO authenticated USING (user_id = auth.uid()); CREATE POLICY &quot;Allow users to insert trips&quot; ON trips FOR INSERT TO authenticated WITH CHECK (user_id = auth.uid());&lt;/li&gt;
&lt;li&gt;You would configure similar policies for groups, photos, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Create or open a Supabase project&lt;/b&gt; and enable the uuid-ossp extension.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Use either the SQL Editor or the Table Editor&lt;/b&gt; to define tables (users, trips, groups, photos, logs).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Verify the tables and relationships&lt;/b&gt; under the Database or Table Editor view.&lt;/li&gt;
&lt;li&gt;(Optional) &lt;b&gt;Set up Row Level Security&lt;/b&gt; and Policies to control access.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;By following these steps, you&amp;rsquo;ll have a fully defined schema in Supabase with the tables and relationships matching the design you finalized. From there, you can integrate the Supabase JS client in your Next.js or other application code to perform CRUD operations.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 모든 테이블의 생성까지 완료하면, DB 세팅이 완료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 백엔드 API를 만들어서 DB와 데이터를 주고받는 기능을 만들겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI로 1인 개발</category>
      <category>1인개발</category>
      <category>AI</category>
      <category>AI개발</category>
      <category>ChatGPT</category>
      <category>COPILOT</category>
      <category>LLM</category>
      <author>스튜디오 폴라리스</author>
      <guid isPermaLink="true">https://polaris-workshop.tistory.com/3</guid>
      <comments>https://polaris-workshop.tistory.com/3#entry3comment</comments>
      <pubDate>Mon, 14 Apr 2025 23:31:09 +0900</pubDate>
    </item>
    <item>
      <title>AI로 앱 만들기 #2 - ChatGPT 프로젝트, 개발 환경 설정하기</title>
      <link>https://polaris-workshop.tistory.com/2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기획 문서를 완료했으니, 개발 환경을 VSCode에 세팅해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 개발 환경을 설정하기 전에 앞으로 프로젝트 전체를 지원하는 GPT 프로젝트를 설정해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;GPT 프로젝트 설정하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT에서 프로젝트를 사용하는 이유는 크게 아래의 두가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 참고해야 하는 문서를 미리 업로드해서 모든 컨텍스트 상에서 문서를 사용할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 전체 프로젝트 및 작업에서 최상위로 처리되는 프롬프트를 작성해 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 이 작업을 해 두지 않으면 LLM 모델의 컨텍스트 용량으로 인해 앞에서 파일 참조를 프롬프트로 넣어두었어도 뒤에 가면 그 프롬프트가 무시되거나, 프로젝트의 기본 목적을 잊고 답변을 주어서 다시 초기 명령을 넣어줘야 하는 경우가 발생하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에, 전체 프로젝트 상에서 기본이 되는 설정 프롬프트를 프로젝트에서 입력해둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 받았던 정보를 기반으로 문서를 여러개로 쪼개줍니다. 저는 크게는 기획 문서와 개발 문서로 나누고 각각 목적별로 나누었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조로 만들어보면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기획 문서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 개요 및 핵심 기능&lt;/li&gt;
&lt;li&gt;유저 스토리와 인수 조건&lt;/li&gt;
&lt;li&gt;정책과 알고리즘&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개발 문서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 아키텍처&lt;/li&gt;
&lt;li&gt;기술 스택&lt;/li&gt;
&lt;li&gt;개발 환경&lt;/li&gt;
&lt;li&gt;배포 환경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 파일을 txt로 만들어서 프로젝트의 &quot;프로젝트 파일&quot;에 업로드합니다. 그리고 이 파일을 사용하는 기본 설정 프롬프트를 &quot;지침&quot; 부분에 작성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지침 프롬프트는 다음과 같이 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[프로젝트 지침 프롬프트]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744120751659&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;당신은 1인 개발자입니다. Visual Studio Code를 IDE로 사용하여 모바일 호환 웹 서비스를 개발합니다.
이 서비스의 핵심 기능은 다음과 같습니다.
이 핵심 기능과 업로드된 참조 문서를 참고하여 저와 함께 서비스를 개발해 주세요.

[핵심 기능] 
- 업로드된 여행 사진의 촬영 시간과 위치 데이터를 분석하여 시간과 위치별로 사진을 그룹화 
- 각 그룹에 방문한 장소에 대한 리뷰 남기기 
- 이 여행을 시간 순서와 공간 순서로 정리하고, 각 그룹의 리뷰를 포함하여 전체 여행을 한눈에 볼 수 있도록 구성
- 정리된 내용을 바탕으로 LLM 생성형 AI를 사용하여 전체 여행을 정리하는 기록을 생성
- 생성된 기록을 소셜 미디어에 공유

[참고 문서]
- 서비스 개요 및 핵심 기능: SERVICE OVERVIEW &amp;amp; CORE FEATURES.txt
- 사용자 스토리와 승인 기준: USER STORIES AND ACCEPTANCE CRITERIA PER PAGE.txt
- 각 승인 기준에 대한 정책과 알고리즘: POLICIES AND ALGORITHMS FOR ACCEPTANCE CRITERIA.txt
- 시스템 아키텍처 개요: dev_1_SYSTEM ARCHITECTURE OVERVIEW.txt
- 기술 스택: dev_2_TECH STACK.txt
- 개발 환경: dev_3_DEVELOPMENT ENVIRONMENT.txt
- 배포 환경 및 운영: dev_4_DEPLOYMENT AND OPERATIONS, DEVELOPER NOTES.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9Tgci/btsNcgWJfE1/DvrnSrI2AWF79TkNnQQiWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9Tgci/btsNcgWJfE1/DvrnSrI2AWF79TkNnQQiWk/img.png&quot; data-alt=&quot;프로젝트 설정창&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9Tgci/btsNcgWJfE1/DvrnSrI2AWF79TkNnQQiWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9Tgci%2FbtsNcgWJfE1%2FDvrnSrI2AWF79TkNnQQiWk%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;863&quot; height=&quot;228&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 설정창&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 설정했으면 이 프로젝트 내에서 GPT와 대화를 하면서 개발 환경을 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개발 환경 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 프롬프트를 사용하여 개발 환경 설정을 단계별로 요청합니다. 만약, 더 필요한 부분이 있다면 해당 단계에 대해 더 자세하게 알려달라고 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[개발 환경 설정 프롬프트]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744120954853&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;저는 Visual Studio Code를 사용합니다.
현재 아무런 설정 없으며, 프로젝트 폴더 이름은 trip_track입니다.
개발에 필요한 것들을 설정하기 위한 과정을 단계별로 알려주세요.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;o1 모델을 사용하여 요청하였기 때문에 굉장히 답변이 길고 자세하게 나와 내용은 생략합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 목차만 정리해보면 이런 순서와 내용으로 답변이 생성되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. VS Code 초기 설정(Node.js 설치 및 git 리포지토리 초기화)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Next.js + TypeScript 프로젝트 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 라이브러리 및 패키지 설치(관련 패키지 설치, UI 프레임워크 설정)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 환경 변수 설정(API키 작성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 페이지와 API 라우트 구조 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 유저 스토리에 따른 페이지별 개발 내역 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 데이터베이스/파일 스토리지 설정 및 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. OpenAI 클라이언트 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9. 테스트 &amp;amp; QA&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10. 배포 설정(Vercel 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 기본적인 개발 환경을 설정하였고, 이를 설정하는 과정에서 아래와 같은 내용들을 업데이트하였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI Framework: Chakra UI 사용&lt;/li&gt;
&lt;li&gt;데이터베이스/스토리지: Supabase 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용을 다시 위의 txt 파일에 반영하여 프로젝트 파일에 업로드해 주는 것으로 향후 개발 작업을 시작하기 위한 프로젝트와 개발 환경 설정을 마쳤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI로 1인 개발</category>
      <category>1인개발</category>
      <category>AI</category>
      <category>AI개발</category>
      <category>ChatGPT</category>
      <category>COPILOT</category>
      <category>LLM</category>
      <author>스튜디오 폴라리스</author>
      <guid isPermaLink="true">https://polaris-workshop.tistory.com/2</guid>
      <comments>https://polaris-workshop.tistory.com/2#entry2comment</comments>
      <pubDate>Tue, 8 Apr 2025 23:10:02 +0900</pubDate>
    </item>
    <item>
      <title>AI로 앱 만들기 #1 - 기획문서 만들기</title>
      <link>https://polaris-workshop.tistory.com/1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 스튜디오 폴라리스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스튜디오 폴라리스는 AI를 비롯한 다양한 컨텐츠를 도서, 앱서비스, 영상 등의 다양한 방식으로 제공하는 소규모 컨텐츠 제작/개발자 모임입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 글로, 현재 실시간으로 작업중인 앱서비스 개발 이야기를 함께 나눠보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 모바일 앱서비스의 프로덕트 매니저로 일해왔으며, 최근 AI를 이용한 1인 개발을 여러가지 시도해 보고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 관련 지식은 기초적인 수준으로, 사용 가능한 개발언어는 Flutter(기초), Python(데이터 분석 및 자동화&amp;nbsp; 수준) 정도입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트의 목적은 &quot;정말로 한 명이 기초적인 개발 지식을 가지고 앱서비스를 만들고 런칭할 수 있는가?&quot;를 검증해보기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 어느 정도 알고 있는 언어를 사용하지는 않고 온전히 AI에서 제공하는 가이드를 따라 생성을 진행할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서 사용하는 AI 서비스 환경은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ChatGPT Plus&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Github Copilot Pro&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Perplexity Pro&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Copilot Pro를 사용하기 위해 IDE는 Visual Studio Code를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로, ChatGPT의 프로젝트 기능을 사용하여 다양한 기획, 개발의 기반이 되는 문서들을 업로드하고 이 문서들을 RAG 방식으로 활용하여 개발을 진행할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, Copilot에서는 ChatGPT의 프로젝트 파일을 활용할 수 없으므로 별도 폴더에 문서들을 넣어두고 필요할 때마다 참조하여 개발하도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7Wzik/btsNcJ33zho/X0QKViPArzlQDkVgqOPaqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7Wzik/btsNcJ33zho/X0QKViPArzlQDkVgqOPaqK/img.png&quot; data-alt=&quot;ChatGPT 프로젝트 기능을 사용하는 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7Wzik/btsNcJ33zho/X0QKViPArzlQDkVgqOPaqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7Wzik%2FbtsNcJ33zho%2FX0QKViPArzlQDkVgqOPaqK%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;934&quot; height=&quot;453&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ChatGPT 프로젝트 기능을 사용하는 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1단계 - 기초 문서 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초에 서비스가 어떠한 핵심 기능을 할 것인지 정도의 간단한 니즈만 가지고 초기 문서를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서는 ChatGPT의 &quot;심층 리서치&quot; 기능을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[심층 리서치 프롬프트]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744033719855&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;여행에서 찍은 사진을 정리하고, 다른 사람에게 내용을 공유하는 서비스를 만들고 싶어.
이 서비스에 대한 핵심 기능은 다음과 같아.


[핵심 기능]
    - 업로드한 여행 사진들의 촬영 시간과 위치 데이터를 분석하여 시간선 및 위치에 따른 사진 그룹 짓기
    - 각 그룹별로 방문지, 방문지에 대한 평가를 남기기
    - 이 여행을 시간순, 공간순으로 정렬하고 각 그룹의 평가를 포함하여 여행 전체를 한 눈에 볼 수 있도록 정리하기
    - 정리된 내용을 기반으로 LLM 생성형 AI를 사용하여 여행 전체를 정리하는 기록을 만들기
    - 만들어진 기록을 SNS에 공유하기


1인 개발자로써, 혼자 서비스를 개발하기 위해 서비스 기획과 개발에 필요한 인프라 구성 등의 모든 기초 작업 준비를 해 줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 내용을 구체화하기 위한 추가 질문 후에, 다음과 같은 멋진 문서를 만들어주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용을 검토하여 제가 최초에 기대했던 것 혹은 상상했던 것과 차이가 있는지 확인합니다. 다행히, 결과물에서 큰 차이는 없어 보입니다. 내용이 길어 접어두었으므로, 자세한 내용이 보고 싶으시면 펼쳐서 읽어봐주세요.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;0&quot; data-end=&quot;39&quot;&gt;여행 사진 정리 및 SNS 공유 웹 서비스: 기획 및 아키텍처 제안&lt;/h1&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;41&quot; data-end=&quot;50&quot; data-ke-size=&quot;size26&quot;&gt;서비스 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;1인 개발자가 손쉽게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여행 사진을 웹에서 정리&lt;/b&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자동으로 여행 기록을 생성&lt;/b&gt;하여 SNS에 공유할 수 있는 웹 기반 서비스입니다. 사용자는 스마트폰으로 촬영한 원본 사진들(JPEG/HEIC 등)을 업로드하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사진의 EXIF 메타데이터&lt;/b&gt;(촬영 시간, GPS 위치)를 활용하여 자동으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;시간대별&amp;middot;장소별 그룹화&lt;/b&gt;를 수행합니다. 각 그룹에 대해 사용자가 방문지 명칭과 간단한 평가(후기나 별점 등)를 입력하면, 서비스가 전체 여행 일지를 시간 순으로 정리하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;생성형 AI&lt;/b&gt;를 통해 마치 여행 일기처럼 서술된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여행 기록 텍스트&lt;/b&gt;를 자동 생성해 줍니다. 사용자는 이 자동 생성된 글을 필요에 따라 편집한 뒤,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;SNS에 공유&lt;/b&gt;하여 다른 사람들과 여행 추억을 쉽게 나눌 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;450&quot; data-end=&quot;458&quot; data-ke-size=&quot;size26&quot;&gt;핵심 기능&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;459&quot; data-end=&quot;1516&quot;&gt;
&lt;li data-start=&quot;459&quot; data-end=&quot;675&quot;&gt;&lt;span&gt;&lt;b&gt;EXIF 기반 사진 그룹화:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;업로드된 여행 사진들의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;촬영 시간&lt;/b&gt;과 **위치 정보(GPS)**를 읽어와, 일정 시간 범위 내이면서 장소가 가까운 사진들을 하나의 이벤트로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;타임라인(시간선) 및 위치별 그룹화&lt;/b&gt;합니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://stackoverflow.com/questions/618054/algorithm-for-clustering-pictures-based-on-date-taken#:~:text=You%20can%20apply%20pretty%20much,take%20a%20regular%20euclidean%20distance&quot;&gt;&lt;span&gt;stackoverflow.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. 예를 들어, 같은 날 같은 지역에서 촬영된 사진들은 한 그룹으로 묶습니다.&lt;/li&gt;
&lt;li data-start=&quot;676&quot; data-end=&quot;825&quot;&gt;&lt;span&gt;&lt;b&gt;그룹 별 방문지명 및 평가 입력:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;자동으로 생성된 각 사진 그룹마다 사용자가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;방문한 장소 이름&lt;/b&gt;과 해당 장소에 대한 **평가(리뷰 또는 평점)**를 입력할 수 있는 기능을 제공합니다. 이를 통해 사진 묶음에 의미를 부여하고 개인적인 소감을 추가합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;826&quot; data-end=&quot;1006&quot;&gt;&lt;span&gt;&lt;b&gt;여행 전체 기록 및 정렬:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;모든 그룹을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;시간 및 장소 순서대로 정렬&lt;/b&gt;하여 하나의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여행 타임라인&lt;/b&gt;을 구성합니다. 예를 들어 Day1 어디서 무엇을 했고, Day2 어디를 방문했는지가 순차적으로 정리됩니다. 각 그룹에는 사용자가 입력한 방문지명과 평가가 포함되어,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;전체 여행 일지&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 묶입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;1007&quot; data-end=&quot;1234&quot;&gt;&lt;span&gt;&lt;b&gt;생성형 AI 기반 여행기 텍스트 생성:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;정리된 여행 타임라인 데이터를 바탕으로 **대규모 언어 모델(LLM)**을 활용한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여행기 자동 생성&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기능을 제공합니다. 즉, 각 날의 일정과 느낌을 종합하여 AI가 마치 사람의 여행 일지처럼 서술형 문장을 생성합니다. 예를 들어 &amp;ldquo;첫째 날 우리는 에펠탑을 방문했고 매우 인상적이었다&amp;rdquo;와 같이, 사용자 사진과 입력 정보를 근거로 한 문장이 생성됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;1235&quot; data-end=&quot;1516&quot;&gt;&lt;span&gt;&lt;b&gt;편집 및 SNS 공유:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;자동 생성된 여행 기록 텍스트를 사용자가 필요에 따라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;편집&lt;/b&gt;할 수 있는 에디터 기능과, 최종 확정된 여행기를 손쉽게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;SNS에 공유&lt;/b&gt;할 수 있는 기능을 제공합니다. 공유 시 여행기의 텍스트와 대표 사진 등이 포함된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;공유용 페이지&lt;/b&gt;가 생성되고, 사용자는 링크를 복사하거나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Web Share API&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등을 통해 페이스북, 트위터, 인스타그램 등으로 손쉽게 보낼 수 있습니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://2dowon.github.io/docs/javascript/share-by-sns/#:~:text=Web%20Share%20API%EB%A5%BC%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%20%EB%B0%A9%EB%B2%95%EC%9D%80,%EB%A7%A4%EC%9A%B0%20%EA%B0%84%EB%8B%A8%ED%95%98%EB%8B%A4&quot;&gt;&lt;span&gt;2dowon.github.io&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;1518&quot; data-end=&quot;1537&quot; data-ke-size=&quot;size26&quot;&gt;시스템 아키텍처 및 기술 스택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;전체 시스템은 클라이언트 중심으로 동작&lt;/b&gt;하여, **프론트엔드(클라이언트)**와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;백엔드(서버)&lt;/b&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;외부 AI 요약 API&lt;/b&gt;로 구성됩니다. 1인 개발자가 효율적으로 개발/운영할 수 있도록 **서버리스 및 BaaS(Backend as a Service)**를 적극 활용합니다. 아래는 권장 구성 요소와 기술 스택입니다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;1728&quot; data-end=&quot;5914&quot;&gt;
&lt;li data-start=&quot;1728&quot; data-end=&quot;2073&quot;&gt;&lt;span&gt;&lt;b&gt;프론트엔드 (웹 클라이언트):&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자의 웹 브라우저에서 동작하는 싱글 페이지 애플리케이션(SPA) 또는 프로그레시브 웹앱(PWA) 형태로 구현합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;React&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프레임워크 기반의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Next.js&lt;/b&gt;를 사용하면 개발 생산성과 성능을 모두 확보할 수 있습니다. Next.js를 통해 UI를 구성하고, 페이지 간 내비게이션, 상태 관리 등을 처리합니다. 스타일링은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Tailwind CSS&lt;/b&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Chakra UI&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 라이브러리를 활용하여 모바일 친화적인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;반응형 웹&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;디자인으로 구현합니다. 사진 파일 선택/업로드, 미리보기, 텍스트 편집 등의 풍부한 기능을 UI 상에서 제공할 것입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;2075&quot; data-end=&quot;2589&quot;&gt;&lt;span&gt;&lt;b&gt;EXIF 메타데이터 처리 (클라이언트):&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사진 이미지를 업로드하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클라이언트 측에서 EXIF 메타데이터를 추출&lt;/b&gt;합니다. 사진 파일은 &amp;lt;u&amp;gt;사용자 단말기 내에서 처리되며 서버로 원본이 전송되기 전에 메타데이터를 분석&amp;lt;/u&amp;gt;합니다. 이를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JavaScript EXIF 파서 라이브러리&lt;/b&gt;(예: ExifReader 또는 exif-js)를 사용합니다. 실제 사례로, 한 개발자는 웹 브라우저 상에서 선택된 이미지의 EXIF 데이터를 파싱하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ExifReader&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라이브러리를 활용하여 모든 처리를 클라이언트에서 수행한 바 있습니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://hturan.com/writing/exploring-exif#:~:text=The%20following%20visualization%20shows%20the,ExifReader%20library%20by%20Mattias%20Wallander&quot;&gt;&lt;span&gt;hturan.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. 이 단계에서 얻은 촬영 시간(DateTimeOriginal)과 GPS 좌표(GPSLatitude, GPSLongitude) 정보를 배열로 저장합니다. 클라이언트는 이러한 메타데이터를 기반으로 즉각적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사진 그룹화 연산&lt;/b&gt;을 수행합니다 (자바스크립트로 구현).&lt;/li&gt;
&lt;li data-start=&quot;2591&quot; data-end=&quot;3105&quot;&gt;&lt;span&gt;&lt;b&gt;사진 그룹화 로직:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그룹화는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;시간 간격&lt;/b&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;공간적 거리&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 가지를 고려합니다. 일정 시간(예: 2~3시간) 이내에 촬영되고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GPS 좌표상 서로 가까운&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사진들은 한 그룹으로 묶습니다. 구현상으로는 시간값(타임스탬프) 차이와 위치 좌표 간 거리를 계산하여, 두 사진 사이의 &amp;ldquo;이벤트 거리&amp;rdquo;를 정의하고 임계값 이하이면 동일 이벤트로 간주합니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://stackoverflow.com/questions/618054/algorithm-for-clustering-pictures-based-on-date-taken#:~:text=You%20can%20apply%20pretty%20much,take%20a%20regular%20euclidean%20distance&quot;&gt;&lt;span&gt;stackoverflow.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. 예를 들어 연속된 날들에 걸쳐 촬영되었지만 장소 이동이 거의 없는 경우 하나의 연속된 이벤트로 묶고, 촬영 날짜 사이에 긴 공백이 있거나 촬영 장소가 수백 km 떨어져 있으면 별개의 그룹으로 분리합니다. (필요 시 k-means 등의 클러스터링 기법을 적용하여 자동 그룹 개수를 산정할 수도 있습니다.) 그룹화 결과는 클라이언트 메모리 상에 구조화된 데이터(예: Trip -&amp;gt; Day 1 -&amp;gt; [장소1 그룹, 장소2 그룹] -&amp;gt; Day 2 -&amp;gt; ...)로 유지됩니다.&lt;/li&gt;
&lt;li data-start=&quot;3107&quot; data-end=&quot;4396&quot;&gt;&lt;span&gt;&lt;b&gt;백엔드 서버 및 API:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;백엔드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버리스 함수&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;경량 서버&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 구성하여 주로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;AI 요약 요청 처리&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터 영속화&lt;/b&gt;를 담당합니다. Next.js를 사용할 경우 내장 API Routes를 통해 서버리스 함수들을 구현할 수 있고, 별도의 백엔드 프레임워크로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Node.js + Express&lt;/b&gt;를 사용하여 REST API를 구성하는 것도 가능합니다. 1인 개발자의 운영 부담을 줄이기 위해,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Vercel&lt;/b&gt;과 같은 플랫폼에 이 백엔드 기능을 배포하면 자동으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;확장 가능한 서버리스 API&lt;/b&gt;가 됩니다. Vercel 플랫폼은 프론트엔드 배포와 함께 서버리스 함수를 제공하고 글로벌 CDN, 자동 배포 등을 지원하여 서버 관리의 복잡성을 크게 줄여줍니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/cloudvillains/supabase%EC%99%80-vercel%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-73ec22ac4f56#:~:text=Vercel%20%EC%9D%80%20%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%20%EA%B0%9C%EB%B0%9C%EC%97%90%20%EC%B4%88%EC%A0%90%EC%9D%84,%ED%86%B5%ED%95%B4%20%EC%84%9C%EB%B2%84%20%EA%B4%80%EB%A6%AC%EC%9D%98%20%EB%B3%B5%EC%9E%A1%EC%84%B1%EC%9D%84%20%EC%A4%84%EC%97%AC%EC%A4%8D%EB%8B%88%EB%8B%A4&quot;&gt;&lt;span&gt;medium.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. 백엔드는 주요 기능으로:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;3561&quot; data-end=&quot;4396&quot;&gt;
&lt;li data-start=&quot;3561&quot; data-end=&quot;3816&quot;&gt;&lt;span&gt;업로드된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원본 사진 저장&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(필요 시): 사용자가 사진 업로드를 완료하면, 선택적으로 사진 파일들을 클라우드 스토리지에 업로드하는 API를 제공합니다. (EXIF는 이미 추출되었으므로 이미지 파일은 이때 전송) 대용량 이미지를 효율적으로 다루기 위해 업로드 전에 클라이언트에서 이미지 리사이징/압축(canvas나 browser-image-compression 활용)하거나, 서버에서 수신 후 썸네일 생성 작업을 비동기로 처리할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;3819&quot; data-end=&quot;4095&quot;&gt;&lt;span&gt;&lt;b&gt;OpenAI API 연동:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;여행 타임라인과 사용자 입력 정보를 받아&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;OpenAI 등의 LLM API&lt;/b&gt;에 요청을 보내 여행기 텍스트를 생성합니다. 예를 들어 OpenAI의 GPT-4 모델을 사용하여 프롬프트에 &quot;다음 여행 일정과 장소 평가를 참고하여 한국어 여행 후기를 작성해줘&quot;와 같은 지침과 함께 날짜별 방문지와 메모를 전달하면, 모델이 자연스러운 서술형 텍스트를 반환합니다. 이 요청은 백엔드에서 발생하며, API 키 등의 보안 정보는 백엔드에 저장되어 안전하게 호출됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;4098&quot; data-end=&quot;4232&quot;&gt;&lt;span&gt;&lt;b&gt;데이터베이스 연동:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자의 여행 기록(그룹화된 구조 및 생성된 텍스트 등)을 저장하고 불러오는 API를 제공합니다. 이를 통해 사용자가 편집을 완료한 최종 여행기를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB에 저장&lt;/b&gt;하고, 고유 URL로 공유할 수 있게 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;4235&quot; data-end=&quot;4396&quot;&gt;&lt;span&gt;&lt;b&gt;SNS 공유용 페이지 제공:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;특정 여행 기록을 식별하는 ID 또는 슬러그를 받아 그 내용(텍스트 및 사진 URL 등)을 응답하는 API 혹은 서버사이드 렌더링 페이지를 제공합니다. 이를 통해 다른 사용자가 공유 링크에 접속하면 해당 여행기의 내용을 웹 페이지로 볼 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;4398&quot; data-end=&quot;5160&quot;&gt;&lt;span&gt;&lt;b&gt;데이터베이스 및 스토리지:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자별 여행 기록 데이터(예: 여행 제목, 각 방문지 명, 평가, 생성된 본문 텍스트 등)는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터베이스&lt;/b&gt;에 저장합니다. 1인 개발 환경에서 관리가 용이하도록 **Managed DB(BaaS)**를 사용합니다. 예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Supabase&lt;/b&gt;를 선택하면, PostgreSQL 기반의 호스팅 DB를 사용하면서 자동으로 RESTful API와 인증, 파일 스토리지 등을 활용할 수 있습니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/cloudvillains/supabase%EC%99%80-vercel%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-73ec22ac4f56#:~:text=Supabase%20%EB%8A%94%20%EC%98%A4%ED%94%88%20%EC%86%8C%EC%8A%A4%20BaaS%EB%A1%9C%EC%84%9C%2C,%EC%9E%90%EC%B2%B4%20%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EC%97%AC%20%EC%82%AC%EC%9A%A9%ED%95%A0%20%EC%88%98%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;&lt;span&gt;medium.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. Supabase는 Firebase의 대안으로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;PostgreSQL&lt;/b&gt;에 기반한 저장소, 인증, 실시간 구독, 서버리스 함수까지 백엔드에 필요한 기능을 제공하므로, 복잡한 서버 구축 없이도 데이터 관리를 할 수 있습니다​
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/cloudvillains/supabase%EC%99%80-vercel%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-73ec22ac4f56#:~:text=Supabase%20%EB%8A%94%20%EC%98%A4%ED%94%88%20%EC%86%8C%EC%8A%A4%20BaaS%EB%A1%9C%EC%84%9C%2C,%EC%9E%90%EC%B2%B4%20%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EC%97%AC%20%EC%82%AC%EC%9A%A9%ED%95%A0%20%EC%88%98%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;&lt;span&gt;medium.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. 대안으로 NoSQL이 편하다면 Firebase Firestore를 사용할 수도 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이미지 파일 저장소&lt;/b&gt;는 Supabase를 사용할 경우 내장 **스토리지(bucket)**에 업로드하며, Firebase의 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Firebase Storage&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는, 일반 클라우드 스토리지(AWS S3 등)를 사용할 수 있습니다. 저장된 이미지는 추후 공유 페이지에서 불러오거나 썸네일로 활용됩니다. (HEIC처럼 웹 호환성이 낮은 포맷의 경우 서버 업로드 시 JPEG 변환을 적용하여 저장하면, 모든 브라우저에서 볼 수 있도록 처리 가능합니다.)&lt;/li&gt;
&lt;li data-start=&quot;5162&quot; data-end=&quot;5538&quot;&gt;&lt;span&gt;&lt;b&gt;생성형 AI 요약 서비스:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;본 서비스의 핵심 차별점인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여행기 자동 생성&lt;/b&gt;은 OpenAI와 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;외부 생성형 AI API&lt;/b&gt;를 활용합니다. OpenAI의 GPT-3.5/GPT-4 모델을 사용하면 사람처럼 유창한 여행 후기를 생성할 수 있습니다. 백엔드 서버에서 AI API를 호출하며, API 응답으로 생성된 텍스트를 받아 클라이언트에 전달합니다. 향후 오픈소스 LLM을 직접 호스팅하는 방향으로도 확장 가능하지만, 초기에는 OpenAI API 활용이 개발 편의와 문장 품질 측면에서 유리합니다. AI 요약 서비스 모듈은 추후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프롬프트 엔지니어링&lt;/b&gt;을 통해 사용자 평가(예: &amp;ldquo;맛있었다&amp;rdquo;, &amp;ldquo;별로였다&amp;rdquo; 등의 감상)를 텍스트에 잘 녹여내도록 조정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;5540&quot; data-end=&quot;5914&quot;&gt;&lt;span&gt;&lt;b&gt;기술 스택 요약:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요약하면 프론트엔드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;TypeScript + React(Next.js)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기반으로 동작하고, 백엔드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Node.js&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;서버리스 함수(또는 경량 서버)로 OpenAI API를 호출하며, 데이터 저장은 **Supabase(Postgres)**와 **스토리지(S3 호환)**를 활용하는 구성이 됩니다. 이러한 조합은 1인 개발자가 전체 시스템을 End-to-End로 구현하기에 충분하며, 필요시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Vercel + Supabase/Firebase&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;조합만으로도 별도 서버 관리 없이 모든 기능을 구현할 수 있습니다 (1인 개발의 경우 해당 조합으로 인프라 지식 없이도 충분히 서비스 제작이 가능함이 커뮤니티에서도 언급되고 있습니다).&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;5916&quot; data-end=&quot;5935&quot; data-ke-size=&quot;size26&quot;&gt;사용자 플로우 및 페이지 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 주요 사용 시나리오는 다음과 같으며, 이에 따라 페이지 및 화면 흐름을 설계합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot; data-start=&quot;5989&quot; data-end=&quot;8985&quot;&gt;
&lt;li data-start=&quot;5989&quot; data-end=&quot;6287&quot;&gt;&lt;span&gt;&lt;b&gt;사진 업로드 페이지:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자 로그인 또는 진입 후 첫 화면에서 여행에서 촬영한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여러 장의 사진을 선택&lt;/b&gt;하여 업로드합니다. 모바일 사용자를 고려하여 &quot;파일 선택&quot; 버튼 외에도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;드래그앤드롭&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;영역이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;모바일에서는 갤러리/카메라 열기&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기능을 지원합니다. 선택된 사진들은 썸네일로 미리보여주고, 사용자가 &quot;업로드 시작&quot;을 누르면 클라이언트 측에서 EXIF 정보를 읽어옵니다. 이때 다량의 사진도 빠르게 처리될 수 있도록 비동기로 진행하며, 진행 상태(예: &amp;ldquo;메타데이터 분석 중&amp;hellip;&amp;rdquo;)를 사용자에게 표시합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;6289&quot; data-end=&quot;6665&quot;&gt;&lt;span&gt;&lt;b&gt;사진 메타데이터 분석 및 그룹화:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;업로드된 사진들의 EXIF 데이터를 클라이언트에서 추출한 뒤,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;즉시 그룹화 알고리즘&lt;/b&gt;을 적용합니다. 이 결과로 여행 기간 동안의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;타임라인&lt;/b&gt;이 생성됩니다. 예를 들어 &amp;ldquo;Day 1 - 2025/04/01&amp;rdquo; 아래에 오전, 오후 등의 하위 그룹이 생길 수 있고 각 그룹에 해당하는 사진들이 연속으로 배열됩니다. UI 상에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;시간순 그룹별로 사진 묶음&lt;/b&gt;이 표시되고, 각 묶음의 대표 사진, 촬영기간, 촬영장소(좌표 -&amp;gt; 지도 API를 통해 지명 변환 시 미리 채울 수 있음) 등을 요약 보여줍니다. 사용자가 필요하면 그룹을 분할하거나 합칠 수 있는 옵션도 제공합니다 (예: 그룹 자동 분류가 부정확한 경우 수동 조정).&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;6667&quot; data-end=&quot;7101&quot;&gt;&lt;span&gt;&lt;b&gt;그룹 정보 입력 페이지:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그룹화가 완료되면, 사용자에게 각 그룹별로 **&amp;ldquo;방문지 이름&amp;rdquo;과 &amp;ldquo;간단한 평가(후기)&amp;rdquo;**를 입력받는 폼을 표시합니다. 예를 들어 첫 번째 그룹에 대해 &amp;ldquo;에펠탑 - 매우 아름답고 인상적이었다 (5/5)&amp;rdquo;와 같이 장소명과 느낌을 적을 수 있습니다. 만약 특정 그룹의 위치 좌표가 존재한다면 좌표를 역지오코딩하여 &amp;ldquo;파리 에펠탑&amp;rdquo;처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;추천 장소 이름&lt;/b&gt;을 제공하고 사용자가 편집하도록 할 수도 있습니다 (이때 Google Maps Geocoding API 등 이용). 각 그룹 카드에는 그 장소를 나타내는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;지도 썸네일&lt;/b&gt;(선택사항, Google Static Maps API 등 활용 가능)이나 대표 사진도 보여주어 맥락을 제공합니다. 사용자가 모든 그룹에 이름/메모 입력을 마치면 &amp;ldquo;여행 일지 생성&amp;rdquo; 버튼을 눌러 다음 단계로 넘어갑니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;7103&quot; data-end=&quot;7398&quot;&gt;&lt;span&gt;&lt;b&gt;여행 기록 생성 (AI 요약):&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자가 여행 타임라인과 그룹별 메모 입력을 완료하고 나면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;백엔드에 여행 요약 생성 요청&lt;/b&gt;을 보냅니다. 이때 프론트엔드에서는 로딩 인디케이터(예: &amp;ldquo;AI가 여행 기록을 작성하고 있습니다&amp;hellip;&amp;rdquo;)를 띄워주고, 백엔드 API 호출을 진행합니다. 백엔드는 앞서 설명한 대로 OpenAI 등의 LLM API에 이 정보를 전달하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서술형 여행기 텍스트&lt;/b&gt;를 생성합니다. 몇 초 가량 소요된 후 백엔드가 완성된 여행기 내용을 응답하면, 프론트엔드는 결과를 받아 다음 화면에 표시합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;7400&quot; data-end=&quot;7842&quot;&gt;&lt;span&gt;&lt;b&gt;여행기 확인 및 편집 페이지:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;생성된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여행 기록 텍스트&lt;/b&gt;를 사용자에게 보여주는 페이지입니다. 이 페이지에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;텍스트 편집 기능&lt;/b&gt;을 제공하여 AI 생성 내용에 사용자가 수정을 가하거나, 추가로 하고 싶은 말을 덧붙일 수 있게 합니다. 간단한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;WYSIWYG 에디터&lt;/b&gt;나 리치텍스트 에디터를 사용하여 글자 스타일, 이모지, 줄바꿈 등을 편집할 수 있으며, 원하는 경우 각 문장 옆에 원본 사진을 배치하거나 그룹 구분을 넣는 등 편집도 가능하게 합니다. 기본적으로는 AI가 만들어준 본문이 일기 형식으로 쭉 나열되어 있고, 날짜별 구분이나 하이라이트는 자동으로 서식화합니다. 예를 들어 날짜 제목은 볼드체 큰 글씨로, 장소명은 이탤릭 처리 등 시각적 구성을 가미합니다. 사용자는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여행 전체 내용을 확인&lt;/b&gt;하면서 잘못된 정보가 있으면 직접 수정한 후, 최종 확정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;7844&quot; data-end=&quot;8692&quot;&gt;&lt;span&gt;&lt;b&gt;SNS 공유 및 저장:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자가 여행기 편집을 완료하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&amp;ldquo;공유하기&amp;rdquo;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;버튼을 눌러 결과를 세상과 공유합니다. 공유하기를 누르는 순간, 프론트엔드는 해당 여행기의 모든 데이터를 백엔드로 전송하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터베이스에 저장&lt;/b&gt;합니다. 백엔드는 이 여행 기록에 대한 고유 ID(예: UUID 혹은 짧은 해시)를 발급하고, 이를 기반으로 접근 가능한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;공유용 페이지 URL&lt;/b&gt;을 생성합니다 (예:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://mytravel.app/share/abcd1234&quot;&gt;https://mytravel.app/share/abcd1234&lt;/a&gt;). 이 URL은 DB에 저장된 여행기 ID에 매핑되어 있어, 누구나 링크를 클릭하면 해당 콘텐츠를 볼 수 있습니다. 공유 페이지에는 여행기 제목, 작성자(옵션), 본문 텍스트, 대표 사진들이 포함됩니다. 또한 원활한 공유를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Open Graph 태그&lt;/b&gt;를 설정하여 SNS에서 미리보기 카드(제목, 썸네일 이미지, 요약)가 보이도록 합니다. 공유 방법으로는 단순히 링크를 복사해갈 수도 있고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;모바일 기기에서는 Web Share API&lt;/b&gt;를 활용하여 OS 레벨의 공유 시트를 열 수 있습니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://2dowon.github.io/docs/javascript/share-by-sns/#:~:text=Web%20Share%20API%EB%A5%BC%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%20%EB%B0%A9%EB%B2%95%EC%9D%80,%EB%A7%A4%EC%9A%B0%20%EA%B0%84%EB%8B%A8%ED%95%98%EB%8B%A4&quot;&gt;&lt;span&gt;2dowon.github.io&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. Web Share API를 사용하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;제목, 텍스트, URL을 간단히 지정&lt;/b&gt;하여 사용자가 설치한 다른 SNS 앱으로 직접 공유할 수 있으므로 모바일 웹에서 편리합니다​
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://2dowon.github.io/docs/javascript/share-by-sns/#:~:text=Web%20Share%20API%EB%A5%BC%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%20%EB%B0%A9%EB%B2%95%EC%9D%80,%EB%A7%A4%EC%9A%B0%20%EA%B0%84%EB%8B%A8%ED%95%98%EB%8B%A4&quot;&gt;&lt;span&gt;2dowon.github.io&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. (만약 Web Share API를 지원하지 않는 환경이라면, 트위터나 페이스북 공유 링크 생성 등의 폴백을 제공합니다.) 이렇게 공유된 여행 기록 페이지는 읽기 전용으로 제공되며, 작성자만 수정할 수 있습니다.&lt;/li&gt;
&lt;li data-start=&quot;8694&quot; data-end=&quot;8985&quot;&gt;&lt;span&gt;&lt;b&gt;공유된 여행 기록 열람:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다른 사용자가 SNS에서 공유된 링크를 클릭하면, 해당 여행 기록의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;공개 페이지&lt;/b&gt;가 렌더링됩니다. 이 페이지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버 사이드 렌더링(SSR)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는 정적 페이지로 제공되어, 로딩 시 즉시 콘텐츠가 표시되고 SEO에도 걸리도록 합니다. 방문자는 여행 일지 텍스트와 사진들을 스크롤하며 볼 수 있고, 반응형 디자인으로 모바일에서도 보기 좋게 구성합니다. 필요하다면 이 페이지에서 &amp;ldquo;나도 내 여행기 만들어보기&amp;rdquo; 같은 **콜투액션(CTA)**을 넣어 서비스 신규 유입을 유도합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;각 페이지는 위 흐름에 맞게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Next.js의 라우트&lt;/b&gt;로 구현할 수 있습니다. 예를 들어 /upload (사진 업로드), /grouping (자동 그룹 확인 및 그룹별 정보 입력), /story (AI 생성 글 확인/편집), /share/[id] (공유용 공개 페이지) 등의 구조로 만들어집니다. 페이지 전환 간에는 상태를 유지해야 하므로, Zustand이나 Redux 같은 상태관리, 또는 URL에 일시적인 세션 토큰을 부여해 서버에 임시 저장하고 다음 페이지에서 불러오는 방법도 고려합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;네비게이션 바&lt;/b&gt;에는 서비스 이름과 간단한 설명, 그리고 (로그인 기능이 있다면) 사용자 프로필 등이 표시되고, 각 단계별로 진행 상황을 표시하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;progress bar&lt;/b&gt;나 스텝 표시를 두어 사용자 경험을 향상시킵니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;9403&quot; data-end=&quot;9419&quot; data-ke-size=&quot;size26&quot;&gt;개발 및 배포 환경 제안&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;9420&quot; data-end=&quot;11679&quot;&gt;
&lt;li data-start=&quot;9420&quot; data-end=&quot;9634&quot;&gt;&lt;span&gt;&lt;b&gt;개발 환경:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로컬에서는 Node.js 기반으로 Next.js 개발 서버를 띄워가며 프론트/백엔드를 한꺼번에 개발합니다. TypeScript를 사용하여 타입 안정성을 높이고, VSCode 등의 에디터와 ESLint/Prettier를 적용해 코드 품질을 유지합니다. Git 저장소로 버전관리를 하며, 주요 브랜치에 푸시하면 자동으로 배포가 이루어지도록 CI/CD를 구성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;9635&quot; data-end=&quot;10098&quot;&gt;&lt;span&gt;&lt;b&gt;배포 플랫폼:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Vercel&lt;/b&gt;을 추천합니다. Vercel은 Next.js 프로젝트와 극대화된 연동성을 갖고 있어, 레포지토리를 연결하면 푸시 시 자동 빌드/배포됩니다. 프론트엔드 정적 파일은 전세계 CDN에 배포되고, API Routes로 작성된 백엔드 함수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버리스로 자동 호스팅&lt;/b&gt;됩니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/cloudvillains/supabase%EC%99%80-vercel%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-73ec22ac4f56#:~:text=Vercel%20%EC%9D%80%20%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%20%EA%B0%9C%EB%B0%9C%EC%97%90%20%EC%B4%88%EC%A0%90%EC%9D%84,%ED%86%B5%ED%95%B4%20%EC%84%9C%EB%B2%84%20%EA%B4%80%EB%A6%AC%EC%9D%98%20%EB%B3%B5%EC%9E%A1%EC%84%B1%EC%9D%84%20%EC%A4%84%EC%97%AC%EC%A4%8D%EB%8B%88%EB%8B%A4&quot;&gt;&lt;span&gt;medium.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. 1인 개발자는 복잡한 서버 설정 없이도 Vercel 대시보드에서 환경변수 관리(예: OpenAI API 키), 도메인 연결, 모니터링 등을 할 수 있습니다. 대안으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Netlify&lt;/b&gt;도 유사한 기능을 제공하며, Next.js 외에 React SPA의 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CloudFront+S3&lt;/b&gt;에 정적 호스팅하고 AWS Lambda로 API를 구성하는 방안도 있지만, 이는 운영 부담이 커지므로 Vercel 같은 올인원 플랫폼이 적합합니다.&lt;/li&gt;
&lt;li data-start=&quot;10099&quot; data-end=&quot;10532&quot;&gt;&lt;span&gt;&lt;b&gt;데이터베이스 및 인증:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Supabase&lt;/b&gt;를 사용하면 배포가 용이합니다. Supabase 프로젝트를 생성한 뒤, 프론트엔드에서 Supabase JS SDK를 통해 손쉽게 Postgres DB에 쿼리하거나 스토리지에 파일 업로드를 할 수 있습니다. Supabase는 테이블 스키마만 정의하면 자동으로 RESTful 엔드포인트가 생기고, 필요한 곳에 RLS(Row-Level Security)나 OAuth 인증 등을 붙일 수 있어 유연합니다. 개발 단계에서는 Supabase의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로컬 Docker 환경&lt;/b&gt;을 활용해 볼 수도 있고, Firebase를 쓴다면 Firebase Emulators로 개발 환경을 꾸립니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;환경 변수&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(예: Supabase URL/키, Firebase API 키 등)는 .env 파일에 저장하고 Vercel 환경설정에 주입합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;10533&quot; data-end=&quot;10921&quot;&gt;&lt;span&gt;&lt;b&gt;AI API 연동:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;OpenAI API는 백엔드(Node.js)에서 openai 라이브러리나 axios로 HTTPS 호출을 통해 사용합니다. OpenAI API 키는 노출되지 않도록 서버 환경변수로만 유지하고, 백엔드 코드는 이 키를 이용해 GPT-4 (또는 3.5 Turbo) 모델의 completion/chat endpoint를 호출합니다. 프롬프트 구성과 모델 선택은 실험을 통해 최적화하고, 생성된 텍스트의 길이에 따라 토큰 수나 비용이 증가하므로 필요하면 일정 길이로 요약하도록 프롬프트를 조정합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;요금 관리&lt;/b&gt;를 위해 OpenAI의 사용량 대시보드 모니터링을 하고, 무료 대안으로 HuggingFace Hub의 번역/요약 모델 API를 고려할 수도 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;10922&quot; data-end=&quot;11349&quot;&gt;&lt;span&gt;&lt;b&gt;이미지 및 정적 파일 호스팅:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자가 업로드한 원본 사진은 Supabase 스토리지 또는 Firebase Storage에 저장됩니다. Supabase의 경우 업로드된 파일에 대해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;퍼블릭 URL&lt;/b&gt;을 부여하거나 공유 페이지 요청 시 서버가 프록시로 전달할 수 있습니다. Supabase 스토리지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이미지 변환 기능&lt;/b&gt;을 제공하여, URL 파라미터로 리사이즈된 이미지를 서빙하는 등&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자동 최적화&lt;/b&gt;가 가능합니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://supabase.com/docs/guides/storage/serving/image-transformations#:~:text=Supabase%20Storage%20offers%20the%20functionality,and%20optimized%20for%20fast%20delivery&quot;&gt;&lt;span&gt;supabase.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. 이를 활용해 공유 페이지에서 원본의 썸네일(저용량) 이미지를 불러와 빠르게 표시하고, 클릭 시 고해상도 원본을 띄우는 식으로 최적화합니다. 정적 파일(예: 서비스 로고, 아이콘 등)은 Vercel에 함께 배포하거나, 별도 CDN에 올려도 무방합니다.&lt;/li&gt;
&lt;li data-start=&quot;11351&quot; data-end=&quot;11679&quot;&gt;&lt;span&gt;&lt;b&gt;배포 워크플로우:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;GitHub 등의 레포지토리와 Vercel/Supabase를 연동해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CI/CD 파이프라인&lt;/b&gt;을 구축합니다. main 브랜치에 코드를 푸시하면 Vercel이 자동으로 최신 코드를 빌드하여 프로덕션에 반영하고, Supabase의 경우 DB 스키마 변경은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;마이그레이션&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;SQL을 코드에 포함시켜 supabase migrate로 배포하거나 대시보드에서 수동 적용합니다. 코드 변경이 빈번하지 않은 1인 프로젝트 특성상 배포 자동화에 큰 장애는 없으며, 필요 시 GitHub Actions로 테스트 -&amp;gt; 빌드 -&amp;gt; 배포 과정을 제어할 수도 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;11681&quot; data-end=&quot;11707&quot; data-ke-size=&quot;size26&quot;&gt;필요한 서드파티 라이브러리 및 API 목록&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;11708&quot; data-end=&quot;13216&quot;&gt;
&lt;li data-start=&quot;11708&quot; data-end=&quot;11856&quot;&gt;&lt;span&gt;&lt;b&gt;ExifReader (JavaScript 라이브러리)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 클라이언트 측에서 사진의 EXIF 메타데이터(촬영 시간, GPS 등)를 파싱하여 획득​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://hturan.com/writing/exploring-exif#:~:text=The%20following%20visualization%20shows%20the,ExifReader%20library%20by%20Mattias%20Wallander&quot;&gt;&lt;span&gt;hturan.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. (대안: exif-js)&lt;/li&gt;
&lt;li data-start=&quot;11857&quot; data-end=&quot;12014&quot;&gt;&lt;span&gt;&lt;b&gt;OpenAI API (GPT-3.5/GPT-4)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 여행 기록 자동 생성을 위한 생성형 AI API. 백엔드에서 호출하여 사용자의 여행 타임라인을 서술형 텍스트로 변환. OpenAI의 공식 Node.js 라이브러리(openai 패키지)나 REST API를 통해 통신.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;12015&quot; data-end=&quot;12261&quot;&gt;&lt;span&gt;&lt;b&gt;Supabase (호스팅 BaaS)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; PostgreSQL 기반 데이터베이스, 파일 스토리지, 인증 등을 제공하는 BaaS​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/cloudvillains/supabase%EC%99%80-vercel%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-73ec22ac4f56#:~:text=Supabase%20%EB%8A%94%20%EC%98%A4%ED%94%88%20%EC%86%8C%EC%8A%A4%20BaaS%EB%A1%9C%EC%84%9C%2C,%EC%9E%90%EC%B2%B4%20%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EC%97%AC%20%EC%82%AC%EC%9A%A9%ED%95%A0%20%EC%88%98%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;&lt;span&gt;medium.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. JS용 SDK (@supabase/supabase-js)로 손쉽게 데이터 입출력 및 파일 업로드 구현. (대안: Firebase &amp;ndash; Firestore DB, Firebase Storage, Firebase Auth 등)&lt;/li&gt;
&lt;li data-start=&quot;12262&quot; data-end=&quot;12368&quot;&gt;&lt;span&gt;&lt;b&gt;React &amp;amp; Next.js 프레임워크&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 사용자 인터페이스 구축과 라우팅, 서버리스 함수(API Routes) 구현에 사용. SSR/SSG 지원을 통해 공유 페이지 SEO 개선.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;12369&quot; data-end=&quot;12517&quot;&gt;&lt;span&gt;&lt;b&gt;UI 컴포넌트 라이브러리&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 신속한 UI 개발을 위해 사용. 예: **Material-UI(MUI)**의 Timeline 컴포넌트 (수직 타임라인 구현에 활용),&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Chakra UI&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Ant Design&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등으로 폼, 버튼, 모달 등 제작.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;12518&quot; data-end=&quot;12735&quot;&gt;&lt;span&gt;&lt;b&gt;지도 및 위치 API&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; (옵션) 사진의 GPS좌표를 실제 지명으로 변환하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Google Maps Geocoding API&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Mapbox Geocoding API&lt;/b&gt;를 사용. 또한 공유 페이지나 그룹 정보 입력 시 지도 화면 표시에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Leaflet&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;+ OpenStreetMap 같은 오픈소스 지도나 Google Maps Embed API를 활용 가능.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;12736&quot; data-end=&quot;12939&quot;&gt;&lt;span&gt;&lt;b&gt;웹 공유 API&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 모바일 환경에서 네이티브 공유 기능을 호출하기 위한 표준 Web API. navigator.share() 메소드를 통해 제목, 텍스트, URL 등을 다른 앱으로 전달​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://2dowon.github.io/docs/javascript/share-by-sns/#:~:text=Web%20Share%20API%EB%A5%BC%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%20%EB%B0%A9%EB%B2%95%EC%9D%80,%EB%A7%A4%EC%9A%B0%20%EA%B0%84%EB%8B%A8%ED%95%98%EB%8B%A4&quot;&gt;&lt;span&gt;2dowon.github.io&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. (폴백으로 카카오 SDK나 Facebook SDK 등을 통한 직접 공유도 고려)&lt;/li&gt;
&lt;li data-start=&quot;12940&quot; data-end=&quot;13081&quot;&gt;&lt;span&gt;&lt;b&gt;이미지 처리 라이브러리&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; (옵션) 클라이언트에서 이미지 리사이즈/압축에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;browser-image-compression&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 패키지 사용. 서버에서 썸네일 생성 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;sharp&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(Node.js 이미지 처리 라이브러리) 사용 가능.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;13082&quot; data-end=&quot;13216&quot;&gt;&lt;span&gt;&lt;b&gt;기타 개발 도구&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 형상관리를 위한 Git 및 GitHub, 배포를 위한 Vercel CLI, 테스트를 위한 Jest/React Testing Library, 오류 모니터링을 위한 Sentry 등이 프로젝트에 포함될 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;13218&quot; data-end=&quot;13239&quot; data-ke-size=&quot;size26&quot;&gt;확장성 고려 및 운영 자동화 방향&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;13240&quot; data-end=&quot;15628&quot;&gt;
&lt;li data-start=&quot;13240&quot; data-end=&quot;13555&quot;&gt;&lt;span&gt;&lt;b&gt;서버리스 및 자동 확장:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;제안된 구조는 서버리스 기반이므로 사용자가 증가해도 자동으로 확장됩니다. 프론트엔드는 정적 파일로 배포되어 대규모 트래픽에도 CDN이 대응하고, 백엔드 API는 호출량에 따라 Vercel이 함수 인스턴스를 늘려 처리합니다. 개발자는 명시적으로 서버 증설을 하지 않아도 되며, Supabase/Firebase 역시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;매니지드 서비스&lt;/b&gt;로서 일정 수준까지 자동 확장됩니다. 데이터베이스의 경우 연결 수나 질의를 고려하여 플랜을 업그레이드하는 것으로 수직 확장 가능하고, 필요하면 읽기 복제본을 추가하여 확장성을 높일 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;13556&quot; data-end=&quot;14050&quot;&gt;&lt;span&gt;&lt;b&gt;이미지 최적화와 CDN:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사진 서비스에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이미지 로딩 최적화&lt;/b&gt;는 중요합니다. Supabase 스토리지의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이미지 트랜스포메이션 기능&lt;/b&gt;을 활용하면 요청 시 파라미터로 리사이즈된 이미지 URL을 생성할 수 있어, 별도의 이미지 썸네일 생성 코드 없이도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;저해상도 이미지 빠른 제공&lt;/b&gt;이 가능합니다​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://supabase.com/docs/guides/storage/serving/image-transformations#:~:text=Supabase%20Storage%20offers%20the%20functionality,and%20optimized%20for%20fast%20delivery&quot;&gt;&lt;span&gt;supabase.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
. 이를 통해 모바일에서도 빠르게 사진 미리보기를 표시하고, 원본은 필요시에만 불러오도록 하여 트래픽을 절약합니다. Firebase Storage를 쓸 경우 Cloud CDN을 앞단에 설정해 캐싱하도록 하고, 또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Cloudinary&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 서비스를 도입해 업로드와 동시에 여러 사이즈로 변환해두는 방안도 있습니다. 모든 이미지 파일과 정적 자원에 적절한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;캐시 만료 헤더&lt;/b&gt;를 설정해 브라우저 캐싱을 활용하고, CDN을 통한 지리적&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;캐시 분산&lt;/b&gt;으로 응답 시간을 최소화합니다.&lt;/li&gt;
&lt;li data-start=&quot;14051&quot; data-end=&quot;14418&quot;&gt;&lt;span&gt;&lt;b&gt;코드 및 인프라 관리 자동화:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Vercel과 GitHub 연동으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Continuous Deployment&lt;/b&gt;가 구현되어, 코드 푸시 시 자동 빌드/배포가 되어 별도의 수동 배포가 필요 없습니다. 또한 Vercel은 자동으로 프리뷰 배포를 제공하므로, 기능 추가/수정 시마다 미리보기 URL로 결과를 검증할 수 있어 개발 효율이 높습니다. 인프라 구성은 대부분 서비스형으로 처리되므로, 코드 레벨에서 IaC(Infrastructure as Code)를 관리할 내용은 최소입니다. 다만 Supabase의 DB 스키마는 supabase/migrations에 SQL로 버전관리를 해두고, 이를 통해 로컬과 프로덕션의 스키마 동기화를 자동화합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;14419&quot; data-end=&quot;14736&quot;&gt;&lt;span&gt;&lt;b&gt;모니터링 및 로깅:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;1인 개발이라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로그 수집과 모니터링&lt;/b&gt;을 설정해 두는 것이 안정성에 도움이 됩니다. Vercel 제공 함수 로그와 Supabase 로그를 수시로 확인할 수 있고, 중요 오류에 대해서는 알림을 받도록 Sentry와 같은 APM(Application Performance Monitoring)을 붙일 수 있습니다. 예를 들어 OpenAI API 호출 실패나 시간 초과 시 에러를 로깅하고 개발자에게 이메일 알림을 보낸다든지, 사용자 행동 로그(업로드 시작~종료, 공유 횟수 등)를 분석해 대시보드로 시각화하면 서비스 운영에 유용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;14737&quot; data-end=&quot;15071&quot;&gt;&lt;span&gt;&lt;b&gt;비용 관리 및 확장 계획:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;초기에는 대부분의 구성요소가 프리 티어 또는 저비용으로 운영될 수 있습니다. 사용량이 증가하면 OpenAI API 비용이 가장 크게 증가할 수 있으므로, 필요시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;캐싱 전략&lt;/b&gt;(동일 여행기 재생성 방지 등)이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;대체 모델&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;활용을 고려합니다. 또한 사용자 증가에 따라 Supabase 요금제가 올라갈 수 있으므로, 일정 임계점에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자체 서버/DB로 마이그레이션&lt;/b&gt;하는 계획도 수립합니다. 하지만 제안된 아키텍처는 벤더 종속을 최소화하여, Next.js 코드는 어디서든 호스팅 가능하고 데이터는 PostgreSQL 호환되어 이전이 비교적 수월합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;15072&quot; data-end=&quot;15305&quot;&gt;&lt;span&gt;&lt;b&gt;백업 및 복구:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;서비스 운영 중 사진 및 글 데이터의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;백업 전략&lt;/b&gt;도 필요합니다. Supabase Postgres의 자동 백업 기능을 활성화하고 주기적으로 별도 저장소에 덤프를 받아 놓거나, Firebase라면 Firestore 백업 스케줄링을 설정합니다. 이미지 파일도 주기적으로 별도 스토리지(예: AWS S3로 복제)하거나, 사용자 단말에 그대로 있으므로 필요시 재업로드 할 수 있게 안내합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;15306&quot; data-end=&quot;15628&quot;&gt;&lt;span&gt;&lt;b&gt;향후 기능 확장:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구조적으로 확장에 대비하여 모듈화를 염두에 둡니다. 예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사용자 인증&lt;/b&gt;(로그인/회원가입)을 초기 버전에선 생략하거나 간단히 소셜 로그인으로 넣되, 추후 확장할 수 있도록 Auth 모듈을 분리해 두고, 여행기 공유 범위(공개/비공개) 설정 기능을 추가할 수 있습니다. 또한 AI 기능도 발전하는 대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여행 요약 외에 사진 캡션 생성, 감정 분석&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등 부가 기능을 붙이기 쉬운 형태로 설계합니다. 이러한 기능 추가시에도 기존 아키텍처에 큰 무리 없이 서버리스 함수 하나 추가, 테이블 하나 추가 등의 방식으로 개발할 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;以上の 제안으로, 1인 개발자가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;웹 기반의 여행 사진 정리 및 공유 서비스&lt;/b&gt;를 효과적으로 구축하고 운영할 수 있을 것입니다. 제안된 아키텍처는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클라이언트 성능 최적화&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;생성형 AI의 효과적 활용&lt;/b&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클라우드 서비스의 자동화&lt;/b&gt;에 중점을 두어, 최소한의 노력으로 사용자에게 높은 편의성을 제공하는 것을 목표로 합니다. 필요한 기술 스택과 인프라를 적절히 활용하면, 혼자서도 충분히 확장성 있고 유지보수 쉬운 서비스를 만들어 낼 수 있을 것입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;참고 자료:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;15909&quot; data-end=&quot;16301&quot; data-is-last-node=&quot;&quot; data-is-only-node=&quot;&quot;&gt;
&lt;li data-start=&quot;15909&quot; data-end=&quot;15984&quot;&gt;&lt;span&gt;EXIF 메타데이터를 브라우저에서 처리하는 사례​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://hturan.com/writing/exploring-exif#:~:text=The%20following%20visualization%20shows%20the,ExifReader%20library%20by%20Mattias%20Wallander&quot;&gt;&lt;span&gt;hturan.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li data-start=&quot;15985&quot; data-end=&quot;16058&quot;&gt;&lt;span&gt;시간&amp;middot;위치 기반의 사진 그룹화 알고리즘 논의​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://stackoverflow.com/questions/618054/algorithm-for-clustering-pictures-based-on-date-taken#:~:text=You%20can%20apply%20pretty%20much,take%20a%20regular%20euclidean%20distance&quot;&gt;&lt;span&gt;stackoverflow.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li data-start=&quot;16059&quot; data-end=&quot;16144&quot;&gt;&lt;span&gt;Supabase (오픈소스 BaaS) 및 Vercel 플랫폼 소개​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/cloudvillains/supabase%EC%99%80-vercel%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-73ec22ac4f56#:~:text=Supabase%20%EB%8A%94%20%EC%98%A4%ED%94%88%20%EC%86%8C%EC%8A%A4%20BaaS%EB%A1%9C%EC%84%9C%2C,%EC%9E%90%EC%B2%B4%20%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EC%97%AC%20%EC%82%AC%EC%9A%A9%ED%95%A0%20%EC%88%98%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;&lt;span&gt;medium.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li data-start=&quot;16145&quot; data-end=&quot;16223&quot;&gt;&lt;span&gt;Web Share API를 통한 웹 콘텐츠 공유 방법​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://2dowon.github.io/docs/javascript/share-by-sns/#:~:text=Web%20Share%20API%EB%A5%BC%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%20%EB%B0%A9%EB%B2%95%EC%9D%80,%EB%A7%A4%EC%9A%B0%20%EA%B0%84%EB%8B%A8%ED%95%98%EB%8B%A4&quot;&gt;&lt;span&gt;2dowon.github.io&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li data-start=&quot;16224&quot; data-end=&quot;16301&quot;&gt;&lt;span&gt;Supabase 스토리지의 이미지 실시간 최적화 기능​&lt;/span&gt;&lt;br /&gt;
&lt;div&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://supabase.com/docs/guides/storage/serving/image-transformations#:~:text=Supabase%20Storage%20offers%20the%20functionality,and%20optimized%20for%20fast%20delivery&quot;&gt;&lt;span&gt;supabase.com&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기획 문서 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기초적인 니즈 및 환경적인 배경을 기반으로 기획 문서를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 기본적으로 유저 스토리를 만들고, 그 유저 스토리를 만족시키는 인수 조건과 정책을 만드는 방향으로 기획을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에, 동일한 방향으로 기획 문서를 만들어냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 모처럼 AI를 사용하므로 이 정책과 조건들을 코딩으로 만들기 좋도록 알고리즘도 함께 만들어봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[기획 문서 생성 프롬프트]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744034095122&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;너는 Product Manager야. 이전에 받은 답변을 기반으로, 아래의 작업을 완수해 줘.
1. 사용자 플로우 및 페이지 구성을 기반으로 각 페이지에서의 User Story와 인수조건을 작성해 줘.
2. 각 인수조건을 개발자가 보고 이해할 수 있도록 정책과 알고리즘을 작성해줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 아래와 같은 기획 문서를 만들어 주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 내용을 검토했을 때, 큰 문제가 없으므로 이 상태로 기획 문서 초안을 저장합니다.(더보기를 누르시면 자세한 내용을 보실 수 있습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 data-end=&quot;337&quot; data-start=&quot;295&quot; data-ke-size=&quot;size26&quot;&gt;1. 사용자 플로우 및 페이지 구성: User Story &amp;amp; 인수 조건&lt;/h2&gt;
&lt;h3 data-end=&quot;357&quot; data-start=&quot;339&quot; data-ke-size=&quot;size23&quot;&gt;(1) 사진 업로드 페이지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1214&quot; data-start=&quot;358&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;537&quot; data-start=&quot;358&quot;&gt;&lt;b&gt;User Story&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;537&quot; data-start=&quot;379&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;487&quot; data-start=&quot;379&quot;&gt;&amp;ldquo;&lt;b&gt;여행을 다녀온 사용자&lt;/b&gt;로서, 스마트폰으로 찍은 사진들을 한 번에 업로드하여, 시간/위치 데이터를 빠르게 분석할 준비를 하고 싶다. 업로드가 편리하고 상태를 확인하기 쉬워야 한다.&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;537&quot; data-start=&quot;490&quot;&gt;목적: 사진을 업로드하여 이후의 메타데이터 추출과 그룹화가 가능한 상태가 되는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1214&quot; data-start=&quot;539&quot;&gt;&lt;b&gt;Acceptance Criteria&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1214&quot; data-start=&quot;569&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;569&quot;&gt;&lt;b&gt;[AC1.1] 업로드 파일 형식 및 용량 검증&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;741&quot; data-start=&quot;609&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;677&quot; data-start=&quot;609&quot;&gt;파일을 드래그&amp;amp;드롭 혹은 &amp;lsquo;파일 선택&amp;rsquo; 버튼으로 업로드할 때, JPG/HEIC 등 지원되는 확장자만 허용해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;683&quot;&gt;각 파일의 용량이 설정된 최대치(예: 10MB)를 초과하면 경고 메시지를 표시하고 업로드를 취소한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;940&quot; data-start=&quot;744&quot;&gt;&lt;b&gt;[AC1.2] EXIF 메타데이터 추출&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;940&quot; data-start=&quot;780&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;846&quot; data-start=&quot;780&quot;&gt;업로드된 사진을 클라이언트 측에서 EXIF 라이브러리(exif-js, ExifReader 등)를 통해 분석한다.&lt;/li&gt;
&lt;li data-end=&quot;940&quot; data-start=&quot;852&quot;&gt;EXIF 정보(촬영 일시, GPS 좌표 등)가 존재하면 &lt;b&gt;성공적으로 파싱&lt;/b&gt;했다는 표시(썸네일 혹은 아이콘)를 보여주고, 없는 파일은 경고 표시를 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1115&quot; data-start=&quot;943&quot;&gt;&lt;b&gt;[AC1.3] 업로드 진행 상황 및 오류 피드백&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1115&quot; data-start=&quot;984&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1040&quot; data-start=&quot;984&quot;&gt;업로드되는 동안 진행 상태(예: 업로드 진행률, &amp;lsquo;메타데이터 추출 중&amp;hellip;&amp;rsquo;)가 표시되어야 한다.&lt;/li&gt;
&lt;li data-end=&quot;1115&quot; data-start=&quot;1046&quot;&gt;오류 발생 시, 오류 메시지를 사용자에게 즉시 표시한다 (예: &amp;ldquo;업로드 실패&amp;rdquo;, &amp;ldquo;지원하지 않는 파일 형식입니다&amp;rdquo; 등).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1214&quot; data-start=&quot;1118&quot;&gt;&lt;b&gt;[AC1.4] 다음 단계 이동&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1214&quot; data-start=&quot;1149&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1214&quot; data-start=&quot;1149&quot;&gt;모든 파일 검증과 EXIF 파싱을 마치면, &amp;ldquo;다음으로&amp;rdquo; 버튼이 활성화되어 &lt;b&gt;그룹화 페이지&lt;/b&gt;로 이동할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1219&quot; data-start=&quot;1216&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1250&quot; data-start=&quot;1221&quot; data-ke-size=&quot;size23&quot;&gt;(2) 사진 메타데이터 분석 및 그룹화 페이지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2018&quot; data-start=&quot;1251&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1430&quot; data-start=&quot;1251&quot;&gt;&lt;b&gt;User Story&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1430&quot; data-start=&quot;1272&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1382&quot; data-start=&quot;1272&quot;&gt;&amp;ldquo;&lt;b&gt;나&lt;/b&gt;는 업로드한 사진들의 촬영 시각과 위치 정보를 바탕으로, 같은 시간대와 장소에서 찍은 사진들을 자동으로 묶어보고 싶다. 필요하다면 내가 직접 그룹을 나누거나 합칠 수도 있어야 한다.&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;1430&quot; data-start=&quot;1385&quot;&gt;목적: 시간/위치 기반 &lt;b&gt;자동 그룹화&lt;/b&gt;와, 사용자의 &lt;b&gt;수동 수정&lt;/b&gt; 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2018&quot; data-start=&quot;1432&quot;&gt;&lt;b&gt;Acceptance Criteria&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2018&quot; data-start=&quot;1462&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1670&quot; data-start=&quot;1462&quot;&gt;&lt;b&gt;[AC2.1] 그룹화 알고리즘 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1670&quot; data-start=&quot;1496&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1575&quot; data-start=&quot;1496&quot;&gt;업로드 완료 시점에 클라이언트에서 시간&amp;middot;위치 기반 알고리즘(예: &amp;lsquo;2시간 이내 &amp;amp; 5km 이내&amp;rsquo;)을 사용해 자동 클러스터링을 수행한다.&lt;/li&gt;
&lt;li data-end=&quot;1670&quot; data-start=&quot;1581&quot;&gt;그룹화 결과를 UI(카드 혹은 목록)로 사용자에게 즉시 보여준다 (예: &amp;lsquo;그룹1(파리 10:00-12:00), 그룹2(에펠탑 12:30-13:00)&amp;rsquo; 등).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1879&quot; data-start=&quot;1673&quot;&gt;&lt;b&gt;[AC2.2] 그룹 분할/병합&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1879&quot; data-start=&quot;1704&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1756&quot; data-start=&quot;1704&quot;&gt;자동 그룹화 결과가 부정확할 경우, 사용자는 그룹을 분할하거나 병합할 수 있어야 한다.&lt;/li&gt;
&lt;li data-end=&quot;1823&quot; data-start=&quot;1762&quot;&gt;그룹 분할 시, 해당 그룹 내 사진들을 분할 기준(시간대/장소)을 지정해 2개 이상의 그룹으로 나눈다.&lt;/li&gt;
&lt;li data-end=&quot;1879&quot; data-start=&quot;1829&quot;&gt;병합 시, 인접 그룹(또는 사용자가 선택한 두 그룹)을 하나로 합쳐 새 그룹을 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2018&quot; data-start=&quot;1882&quot;&gt;&lt;b&gt;[AC2.3] 인터랙션 및 미리보기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2018&quot; data-start=&quot;1916&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1969&quot; data-start=&quot;1916&quot;&gt;각 그룹을 클릭하면 해당 그룹에 속한 &lt;b&gt;사진 썸네일 목록&lt;/b&gt;을 자세히 확인할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2018&quot; data-start=&quot;1975&quot;&gt;그룹화가 완료되면 &amp;ldquo;다음 단계로 이동(그룹 정보 입력)&amp;rdquo; 버튼이 나타난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2023&quot; data-start=&quot;2020&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2045&quot; data-start=&quot;2025&quot; data-ke-size=&quot;size23&quot;&gt;(3) 그룹 정보 입력 페이지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2820&quot; data-start=&quot;2046&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2215&quot; data-start=&quot;2046&quot;&gt;&lt;b&gt;User Story&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2215&quot; data-start=&quot;2067&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2155&quot; data-start=&quot;2067&quot;&gt;&amp;ldquo;&lt;b&gt;나&lt;/b&gt;는 자동으로 생성된 사진 그룹을 확인하고, 각 그룹마다 방문지 이름과 평가(간단 리뷰, 별점 등)를 입력해 여행 기록에 의미를 부여하고 싶다.&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;2215&quot; data-start=&quot;2158&quot;&gt;목적: 사용자 개별 입력(장소명, 후기, 별점 등)을 통해 그룹을 &amp;lsquo;의미 있는 방문지 단위&amp;rsquo;로 확정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2820&quot; data-start=&quot;2217&quot;&gt;&lt;b&gt;Acceptance Criteria&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2820&quot; data-start=&quot;2247&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2392&quot; data-start=&quot;2247&quot;&gt;&lt;b&gt;[AC3.1] 그룹 리스트 표시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2392&quot; data-start=&quot;2279&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2333&quot; data-start=&quot;2279&quot;&gt;그룹화된 결과 목록(그룹 이름, 대표 사진, 촬영 위치/시간 요약)을 사용자에게 표시한다.&lt;/li&gt;
&lt;li data-end=&quot;2392&quot; data-start=&quot;2339&quot;&gt;사용자는 각 그룹을 클릭하거나 펼쳐서 방문지 이름, 평가 등을 기입할 폼을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2560&quot; data-start=&quot;2395&quot;&gt;&lt;b&gt;[AC3.2] 방문지명/평가 입력&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2560&quot; data-start=&quot;2428&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2492&quot; data-start=&quot;2428&quot;&gt;각 그룹에 방문지 이름(예: &amp;ldquo;에펠탑&amp;rdquo;)과 평가(텍스트 후기, 별점 등)를 입력할 수 있는 필드를 제공한다.&lt;/li&gt;
&lt;li data-end=&quot;2560&quot; data-start=&quot;2498&quot;&gt;최소한의 필수 입력(방문지명 등)이 누락될 경우 경고 표시를 하고, &amp;ldquo;AI 요약 생성&amp;rdquo; 버튼을 비활성화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2699&quot; data-start=&quot;2563&quot;&gt;&lt;b&gt;[AC3.3] 위치 자동 변환(선택사항)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2699&quot; data-start=&quot;2600&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2665&quot; data-start=&quot;2600&quot;&gt;(옵션) 그룹의 GPS 좌표를 역지오코딩하여 &amp;ldquo;~동/파리 에펠탑 근처&amp;rdquo; 등의 추천 장소 이름을 미리 채워준다.&lt;/li&gt;
&lt;li data-end=&quot;2699&quot; data-start=&quot;2671&quot;&gt;사용자는 자동 제안값을 수동으로 수정 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2820&quot; data-start=&quot;2702&quot;&gt;&lt;b&gt;[AC3.4] 데이터 저장 &amp;amp; 다음 단계&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2820&quot; data-start=&quot;2739&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2820&quot; data-start=&quot;2739&quot;&gt;모든 그룹에 대한 입력을 마친 후, &amp;ldquo;AI 여행기 생성&amp;rdquo; 버튼을 누르면, 이 정보를 &lt;b&gt;임시 저장&lt;/b&gt;하고 &lt;b&gt;AI 생성 페이지&lt;/b&gt;로 넘어간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2825&quot; data-start=&quot;2822&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2855&quot; data-start=&quot;2827&quot; data-ke-size=&quot;size23&quot;&gt;(4) 여행 기록 생성 (AI 요약) 페이지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3528&quot; data-start=&quot;2856&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3026&quot; data-start=&quot;2856&quot;&gt;&lt;b&gt;User Story&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3026&quot; data-start=&quot;2877&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2968&quot; data-start=&quot;2877&quot;&gt;&amp;ldquo;&lt;b&gt;나&lt;/b&gt;는 시간 순으로 정리된 그룹 정보(방문지명, 평가)를 AI가 종합해서 여행기를 자동으로 써주길 원한다. 이 과정을 간단한 조작만으로 해결하고 싶다.&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;3026&quot; data-start=&quot;2971&quot;&gt;목적: AI(Large Language Model)를 활용하여 &lt;b&gt;여행기 텍스트 초안&lt;/b&gt;을 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3528&quot; data-start=&quot;3028&quot;&gt;&lt;b&gt;Acceptance Criteria&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3528&quot; data-start=&quot;3058&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3206&quot; data-start=&quot;3058&quot;&gt;&lt;b&gt;[AC4.1] 요약 생성 요청&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3206&quot; data-start=&quot;3089&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3157&quot; data-start=&quot;3089&quot;&gt;&amp;ldquo;AI 여행기 생성&amp;rdquo; 버튼을 클릭하면 서버(백엔드)로 여행 그룹 정보(일정, 평가, 날짜, 장소명 등)를 전송한다.&lt;/li&gt;
&lt;li data-end=&quot;3206&quot; data-start=&quot;3163&quot;&gt;전송 시 로딩 인디케이터(&amp;ldquo;AI가 여행기를 작성 중입니다&amp;hellip;&amp;rdquo;)를 표시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3370&quot; data-start=&quot;3209&quot;&gt;&lt;b&gt;[AC4.2] LLM API 호출 및 응답 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3370&quot; data-start=&quot;3250&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3325&quot; data-start=&quot;3250&quot;&gt;백엔드에서 OpenAI API(GPT 등)를 통해 서술형 문장을 생성한 뒤, 성공 시 200 응답으로 여행기 텍스트를 반환한다.&lt;/li&gt;
&lt;li data-end=&quot;3370&quot; data-start=&quot;3331&quot;&gt;요청에 실패할 경우 에러 메시지를 표시하고 재시도 버튼을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3528&quot; data-start=&quot;3373&quot;&gt;&lt;b&gt;[AC4.3] 여행기 초안 표시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3528&quot; data-start=&quot;3405&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3476&quot; data-start=&quot;3405&quot;&gt;AI가 생성한 여행기 텍스트 초안을 사용자의 브라우저에 표시하고, 사용자는 이 초안을 확인/다음 단계로 넘어갈 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;3528&quot; data-start=&quot;3482&quot;&gt;&amp;lsquo;수정/편집&amp;rsquo; 페이지로 이동하기 전이라도 간단히 문단을 확인할 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;3533&quot; data-start=&quot;3530&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;3558&quot; data-start=&quot;3535&quot; data-ke-size=&quot;size23&quot;&gt;(5) 여행기 확인 및 편집 페이지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4122&quot; data-start=&quot;3559&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3702&quot; data-start=&quot;3559&quot;&gt;&lt;b&gt;User Story&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3702&quot; data-start=&quot;3580&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3658&quot; data-start=&quot;3580&quot;&gt;&amp;ldquo;&lt;b&gt;나&lt;/b&gt;는 AI가 생성한 여행기 초안을 보고, 잘못된 부분을 수정하거나, 좀 더 자세한 내용을 추가해 최종 여행기로 확정하고 싶다.&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;3702&quot; data-start=&quot;3661&quot;&gt;목적: AI가 작성한 텍스트를 &lt;b&gt;사용자가 직접 편집&lt;/b&gt;하는 최종 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;4122&quot; data-start=&quot;3704&quot;&gt;&lt;b&gt;Acceptance Criteria&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;4122&quot; data-start=&quot;3734&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3855&quot; data-start=&quot;3734&quot;&gt;&lt;b&gt;[AC5.1] AI가 생성한 본문 로드&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3855&quot; data-start=&quot;3770&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3808&quot; data-start=&quot;3770&quot;&gt;이전 단계에서 생성된 텍스트를 텍스트 에디터 영역에 불러온다.&lt;/li&gt;
&lt;li data-end=&quot;3855&quot; data-start=&quot;3814&quot;&gt;일부 문단이 분리되어 보이도록 기본 서식(날짜별 헤더 등)을 적용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3991&quot; data-start=&quot;3858&quot;&gt;&lt;b&gt;[AC5.2] 사용자 편집 기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3991&quot; data-start=&quot;3890&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3945&quot; data-start=&quot;3890&quot;&gt;에디터에서 글자 수정, 문단 추가/삭제, 스타일 변경(볼드, 이탤릭 등) 등을 할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;3991&quot; data-start=&quot;3951&quot;&gt;사진 혹은 그룹별 대표 이미지 등을 본문에 삽입할 수도 있다(옵션).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;4122&quot; data-start=&quot;3994&quot;&gt;&lt;b&gt;[AC5.3] 내용 저장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4122&quot; data-start=&quot;4022&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4079&quot; data-start=&quot;4022&quot;&gt;&amp;ldquo;저장하기&amp;rdquo; 버튼을 누르면 수정된 텍스트 및 편집 상태가 DB(혹은 임시 스토리지)에 반영된다.&lt;/li&gt;
&lt;li data-end=&quot;4122&quot; data-start=&quot;4085&quot;&gt;저장 완료 후 &amp;ldquo;SNS에 공유하기&amp;rdquo; 버튼 활성화 및 이동 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;4127&quot; data-start=&quot;4124&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;4147&quot; data-start=&quot;4129&quot; data-ke-size=&quot;size23&quot;&gt;(6) 여행기 SNS 공유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4827&quot; data-start=&quot;4148&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4296&quot; data-start=&quot;4148&quot;&gt;&lt;b&gt;User Story&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4296&quot; data-start=&quot;4169&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4260&quot; data-start=&quot;4169&quot;&gt;&amp;ldquo;&lt;b&gt;나&lt;/b&gt;는 완성된 여행기를 내 SNS 계정(페이스북, 트위터 등)으로 쉽게 공유하고 싶다. 다른 사람도 링크 클릭만 하면 내 여행기를 볼 수 있어야 한다.&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;4296&quot; data-start=&quot;4263&quot;&gt;목적: 공개 URL 생성 및 SNS에 &lt;b&gt;원클릭 공유&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;4827&quot; data-start=&quot;4298&quot;&gt;&lt;b&gt;Acceptance Criteria&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;4827&quot; data-start=&quot;4328&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;4488&quot; data-start=&quot;4328&quot;&gt;&lt;b&gt;[AC6.1] 공유용 URL 생성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4488&quot; data-start=&quot;4361&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4440&quot; data-start=&quot;4361&quot;&gt;&amp;ldquo;SNS에 공유하기&amp;rdquo; 버튼 클릭 시 서버로 최종 여행기 데이터를 전송 &amp;rarr; &lt;b&gt;고유 ID&lt;/b&gt;(슬러그 등) 기반으로 공유용 URL을 발급.&lt;/li&gt;
&lt;li data-end=&quot;4488&quot; data-start=&quot;4446&quot;&gt;예: &lt;a href=&quot;https://travelapp.com/share/abc123&quot;&gt;https://travelapp.com/share/abc123&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;4665&quot; data-start=&quot;4491&quot;&gt;&lt;b&gt;[AC6.2] 웹 공유 API 활용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4665&quot; data-start=&quot;4525&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4601&quot; data-start=&quot;4525&quot;&gt;모바일 환경에서 navigator.share()(Web Share API)를 지원하면, 시스템 공유 다이얼로그가 뜨도록 한다.&lt;/li&gt;
&lt;li data-end=&quot;4665&quot; data-start=&quot;4607&quot;&gt;미지원 브라우저에서는 트위터/페이스북/카카오톡 등 &lt;b&gt;각 SNS에 직접 연결&lt;/b&gt;되는 링크를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;4827&quot; data-start=&quot;4668&quot;&gt;&lt;b&gt;[AC6.3] 오픈 그래프 메타태그&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4827&quot; data-start=&quot;4702&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4784&quot; data-start=&quot;4702&quot;&gt;공유용 페이지에 접근했을 때, 대표 이미지를 썸네일로 쓰고, 여행기 제목, 간단 요약이 미리보기 카드에 뜨도록 &lt;b&gt;OG 태그&lt;/b&gt;를 설정한다.&lt;/li&gt;
&lt;li data-end=&quot;4827&quot; data-start=&quot;4790&quot;&gt;공유가 성공하면 &amp;ldquo;공유되었습니다&amp;rdquo; 등의 확인 메시지를 표시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;4832&quot; data-start=&quot;4829&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;4859&quot; data-start=&quot;4834&quot; data-ke-size=&quot;size23&quot;&gt;(7) 공유된 페이지(읽기 전용) 열람&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5440&quot; data-start=&quot;4860&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5007&quot; data-start=&quot;4860&quot;&gt;&lt;b&gt;User Story&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5007&quot; data-start=&quot;4881&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4960&quot; data-start=&quot;4881&quot;&gt;&amp;ldquo;&lt;b&gt;다른 사람&lt;/b&gt;이 SNS에서 공유된 링크를 클릭했을 때, 내 여행 기록 페이지를 보고 사진과 텍스트를 편하게 읽을 수 있으면 좋겠다.&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;5007&quot; data-start=&quot;4963&quot;&gt;목적: &lt;b&gt;공유된 여행 기록&lt;/b&gt;을 웹 페이지 형태로 &lt;b&gt;누구나 열람&lt;/b&gt; 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;5440&quot; data-start=&quot;5009&quot;&gt;&lt;b&gt;Acceptance Criteria&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;5440&quot; data-start=&quot;5039&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;5215&quot; data-start=&quot;5039&quot;&gt;&lt;b&gt;[AC7.1] 공개 페이지 렌더링&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5215&quot; data-start=&quot;5072&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5162&quot; data-start=&quot;5072&quot;&gt;URL /share/:id 형태로 접근 시, 해당 ID에 매핑된 여행기 본문과 사진을 SSR(서버사이드 렌더링) 또는 SSG(정적 페이지)로 표시한다.&lt;/li&gt;
&lt;li data-end=&quot;5215&quot; data-start=&quot;5168&quot;&gt;여행기의 모든 문단, 날짜별 구분, 첨부 사진 등을 순서대로 볼 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;5303&quot; data-start=&quot;5218&quot;&gt;&lt;b&gt;[AC7.2] 반응형 디자인&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5303&quot; data-start=&quot;5248&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5303&quot; data-start=&quot;5248&quot;&gt;PC&amp;middot;모바일&amp;middot;태블릿 등 다양한 화면 크기에서 가독성 있게 페이지가 레이아웃을 조정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;5440&quot; data-start=&quot;5306&quot;&gt;&lt;b&gt;[AC7.3] 작성자만 편집 가능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5440&quot; data-start=&quot;5339&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5386&quot; data-start=&quot;5339&quot;&gt;공유 페이지는 &lt;b&gt;읽기 전용&lt;/b&gt;으로, 일반 방문자는 편집 버튼을 볼 수 없다.&lt;/li&gt;
&lt;li data-end=&quot;5440&quot; data-start=&quot;5392&quot;&gt;작성자가 로그인 상태로 접근하는 경우에만 &amp;ldquo;수정하기&amp;rdquo; 버튼이 노출된다(옵션 기능).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;6290&quot; data-start=&quot;6246&quot; data-ke-size=&quot;size26&quot;&gt;2. 각 인수조건을 위한 정책(Policy)과 알고리즘(Algorithm)&lt;/h2&gt;
&lt;p data-end=&quot;6406&quot; data-start=&quot;6292&quot; data-ke-size=&quot;size16&quot;&gt;아래는 &lt;b&gt;중요하거나 구현 난이도가 있는 인수 조건&lt;/b&gt;을 예시로 들어, 어떻게 처리해야 할지 &lt;b&gt;정책&lt;/b&gt;과 &lt;b&gt;알고리즘&lt;/b&gt;을 간략히 기술한 것입니다. (실제로는 모든 AC에 대해 세분화할 수도 있습니다.)&lt;/p&gt;
&lt;h3 data-end=&quot;6431&quot; data-start=&quot;6408&quot; data-ke-size=&quot;size23&quot;&gt;2.1 사진 업로드 (AC1 계열)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8101&quot; data-start=&quot;6433&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7211&quot; data-start=&quot;6433&quot;&gt;&lt;b&gt;AC1.1 (파일 형식 및 용량 검증)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7211&quot; data-start=&quot;6465&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6646&quot; data-start=&quot;6465&quot;&gt;&lt;b&gt;정책(Policy)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6646&quot; data-start=&quot;6487&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6558&quot; data-start=&quot;6487&quot;&gt;지원 파일 형식: [&quot;.jpg&quot;, &quot;.jpeg&quot;, &quot;.png&quot;, &quot;.heic&quot;] 등 (디자인 결정에 따라 확장 가능)&lt;/li&gt;
&lt;li data-end=&quot;6602&quot; data-start=&quot;6564&quot;&gt;최대 용량: 10MB(또는 5MB 등). 초과 시 즉시 거부.&lt;/li&gt;
&lt;li data-end=&quot;6646&quot; data-start=&quot;6608&quot;&gt;Drag&amp;amp;Drop, File input 양쪽에서 동일 로직 적용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;7211&quot; data-start=&quot;6650&quot;&gt;&lt;b&gt;알고리즘(Algorithm)&lt;/b&gt; (의사코드 예시)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744034276480&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;function handleFileUpload(files):
    allowedExtensions = [&quot;.jpg&quot;, &quot;.jpeg&quot;, &quot;.png&quot;, &quot;.heic&quot;]
    maxSize = 10 * 1024 * 1024  // 10MB

    for file in files:
        ext = getFileExtension(file)
        if ext not in allowedExtensions:
            showError(&quot;지원하지 않는 파일 형식입니다.&quot;)
            continue

        if file.size &amp;gt; maxSize:
            showError(&quot;파일이 너무 큽니다. (최대 10MB)&quot;)
            continue

        readEXIF(file)  // 다음 단계인 EXIF 추출 진행&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8101&quot; data-start=&quot;6433&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8101&quot; data-start=&quot;7212&quot;&gt;&lt;b&gt;AC1.2 (EXIF 메타데이터 추출)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8101&quot; data-start=&quot;7244&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7392&quot; data-start=&quot;7244&quot;&gt;&lt;b&gt;정책(Policy)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7392&quot; data-start=&quot;7266&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7327&quot; data-start=&quot;7266&quot;&gt;클라이언트에서 exif-js 또는 ExifReader 라이브러리를 사용해 EXIF 정보를 파싱.&lt;/li&gt;
&lt;li data-end=&quot;7392&quot; data-start=&quot;7333&quot;&gt;만약 EXIF가 없거나 GPS/촬영일시가 누락된 경우, 후속 단계에서 &quot;메타데이터 없음&quot; 상태로 처리.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;8101&quot; data-start=&quot;7396&quot;&gt;&lt;b&gt;알고리즘(Algorithm)&lt;/b&gt; (의사코드 예시)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744034331604&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function readEXIF(file):
    ExifReader.load(file).then(metadata =&amp;gt; {
        if metadata has GPS info:
            location = extractCoordinates(metadata.GPS)
        else:
            location = null

        if metadata has DateTimeOriginal:
            timeStamp = parseDate(metadata.DateTimeOriginal)
        else:
            timeStamp = file.lastModifiedDate or null

        storeInState({
          file: file,
          location: location,
          timeStamp: timeStamp
        })
    }).catch(error =&amp;gt; {
        showError(&quot;EXIF 추출 중 오류 발생&quot;)
    })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;8106&quot; data-start=&quot;8103&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;8139&quot; data-start=&quot;8108&quot; data-ke-size=&quot;size23&quot;&gt;2.2 메타데이터 분석 및 그룹화 (AC2 계열)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;9161&quot; data-start=&quot;8141&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9161&quot; data-start=&quot;8141&quot;&gt;&lt;b&gt;AC2.1 (시간&amp;middot;위치 기반 그룹화 알고리즘)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;9161&quot; data-start=&quot;8177&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8350&quot; data-start=&quot;8177&quot;&gt;&lt;b&gt;정책(Policy)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8350&quot; data-start=&quot;8201&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8269&quot; data-start=&quot;8201&quot;&gt;일정 기준(예: &lt;b&gt;시간차 2시간 이내&lt;/b&gt;, &lt;b&gt;거리 5km 이내&lt;/b&gt;)를 만족하는 사진들을 하나의 그룹으로 묶는다.&lt;/li&gt;
&lt;li data-end=&quot;8308&quot; data-start=&quot;8275&quot;&gt;제안된 임계값은 사용자 입맛에 맞게 설정/조절 가능.&lt;/li&gt;
&lt;li data-end=&quot;8350&quot; data-start=&quot;8314&quot;&gt;하루를 넘어가면 자동으로 날짜가 바뀌었다고 판단(날짜 구분).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;9034&quot; data-start=&quot;8354&quot;&gt;&lt;b&gt;알고리즘(Algorithm)&lt;/b&gt; (단순 예시: Greedy 방식)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744034364412&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sort photos by timeStamp ascending

set currentGroup = []
set groups = []

for photo in photos:
    if currentGroup is empty:
        currentGroup.add(photo)
    else:
        lastPhoto = currentGroup.last()
        if timeDistance(photo, lastPhoto) &amp;lt;= 2 hours
           AND locationDistance(photo, lastPhoto) &amp;lt;= 5 km:
            currentGroup.add(photo)
        else:
            groups.add(currentGroup)
            currentGroup = [photo]
// 마지막 그룹 추가
if currentGroup not empty:
    groups.add(currentGroup)

return groups&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;9166&quot; data-start=&quot;9163&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;9193&quot; data-start=&quot;9168&quot; data-ke-size=&quot;size23&quot;&gt;2.3 그룹 정보 입력 (AC3 계열)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;9702&quot; data-start=&quot;9195&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9702&quot; data-start=&quot;9195&quot;&gt;&lt;b&gt;AC3.2 (방문지명/평가 입력)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;9702&quot; data-start=&quot;9224&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9370&quot; data-start=&quot;9224&quot;&gt;&lt;b&gt;정책(Policy)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;9370&quot; data-start=&quot;9246&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9289&quot; data-start=&quot;9246&quot;&gt;방문지명은 &lt;b&gt;필수&lt;/b&gt;. 빈 칸이면 AI 요약 단계로 넘어가지 못한다.&lt;/li&gt;
&lt;li data-end=&quot;9320&quot; data-start=&quot;9295&quot;&gt;평가(리뷰 텍스트, 별점 등)는 옵션.&lt;/li&gt;
&lt;li data-end=&quot;9370&quot; data-start=&quot;9326&quot;&gt;(옵션) 욕설/반사회적 단어 필터가 필요하다면 사전에 단어 리스트로 필터링.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;9660&quot; data-start=&quot;9374&quot;&gt;&lt;b&gt;알고리즘(Algorithm)&lt;/b&gt; (간단 검증 예시)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744034398556&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function validateGroupInputs(group):
    if group.placeName is empty:
        return &quot;방문지명을 입력해야 합니다.&quot;
    if containsProfanity(group.reviewText):
        return &quot;부적절한 표현이 포함되어 있습니다.&quot;
    return &quot;OK&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;9707&quot; data-start=&quot;9704&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;9737&quot; data-start=&quot;9709&quot; data-ke-size=&quot;size23&quot;&gt;2.4 AI 여행 기록 생성 (AC4 계열)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;10458&quot; data-start=&quot;9739&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;10458&quot; data-start=&quot;9739&quot;&gt;&lt;b&gt;AC4.2 (LLM API 호출 및 응답 처리)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;10458&quot; data-start=&quot;9776&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9943&quot; data-start=&quot;9776&quot;&gt;&lt;b&gt;정책(Policy)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;9943&quot; data-start=&quot;9800&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9889&quot; data-start=&quot;9800&quot;&gt;OpenAI GPT-3.5/4 API 등 호출 시, &lt;b&gt;사용자 여행정보(장소명, 날짜, 평가)를 프롬프트&lt;/b&gt;에 포함하여 자연스러운 서술형 텍스트를 요청.&lt;/li&gt;
&lt;li data-end=&quot;9943&quot; data-start=&quot;9895&quot;&gt;토큰 비용 관리: 여행 그룹이 너무 많으면 요약할 수 있도록 프롬프트를 최적화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;10401&quot; data-start=&quot;9947&quot;&gt;&lt;b&gt;알고리즘(Algorithm)&lt;/b&gt; (백엔드 Pseudo-code)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744034433881&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function generateTravelStory(groupData):
    prompt = buildPrompt(groupData)  // &quot;다음 방문지 목록을 참고하여 한국어 여행기를 작성...&quot;
    response = openai.createChatCompletion({
      model: &quot;gpt-3.5-turbo&quot;,
      messages: [{role: &quot;user&quot;, content: prompt}],
      max_tokens: 1500,
      temperature: 0.7
    })
    return response.data.choices[0].message.content&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;10463&quot; data-start=&quot;10460&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;10493&quot; data-start=&quot;10465&quot; data-ke-size=&quot;size23&quot;&gt;2.5 여행기 확인 및 편집 (AC5 계열)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;10895&quot; data-start=&quot;10495&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;10895&quot; data-start=&quot;10495&quot;&gt;&lt;b&gt;AC5.2 (사용자 편집 기능)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;10895&quot; data-start=&quot;10523&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;10635&quot; data-start=&quot;10523&quot;&gt;&lt;b&gt;정책(Policy)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;10635&quot; data-start=&quot;10547&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;10593&quot; data-start=&quot;10547&quot;&gt;최소한 단순 텍스트 수정 가능. 필요한 경우 WYSIWYG 편집기를 붙인다.&lt;/li&gt;
&lt;li data-end=&quot;10635&quot; data-start=&quot;10599&quot;&gt;자동 저장 또는 저장 버튼을 두고, 저장 시점에만 DB 반영.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;10895&quot; data-start=&quot;10639&quot;&gt;&lt;b&gt;알고리즘(Algorithm)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;10741&quot; data-start=&quot;10668&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;10741&quot; data-start=&quot;10668&quot;&gt;에디터 자체는 보통 서드파티 라이브러리(예: Quill, TipTap) 사용. 별도 알고리즘보다는 &lt;b&gt;저장 로직&lt;/b&gt;이 중요.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744034490077&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function saveEditedStory(content):
    dbUpdate(userId, travelId, { storyText: content })
    showMessage(&quot;저장 완료&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;10900&quot; data-start=&quot;10897&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;10925&quot; data-start=&quot;10902&quot; data-ke-size=&quot;size23&quot;&gt;2.6 SNS 공유 (AC6 계열)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;11356&quot; data-start=&quot;10927&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;11356&quot; data-start=&quot;10927&quot;&gt;&lt;b&gt;AC6.1 (공유용 URL 생성)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;11356&quot; data-start=&quot;10956&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;11107&quot; data-start=&quot;10956&quot;&gt;&lt;b&gt;정책(Policy)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;11107&quot; data-start=&quot;10980&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;11049&quot; data-start=&quot;10980&quot;&gt;최종 여행 데이터를 &amp;ldquo;공유 허용&amp;rdquo; 상태로 DB에 저장하면, &lt;b&gt;/share/:uniqueId&lt;/b&gt; 형태의 경로를 생성.&lt;/li&gt;
&lt;li data-end=&quot;11078&quot; data-start=&quot;11055&quot;&gt;uniqueId는 UUID나 해시.&lt;/li&gt;
&lt;li data-end=&quot;11107&quot; data-start=&quot;11084&quot;&gt;공유 시 접속 권한을 &amp;lsquo;공개&amp;rsquo;로 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;11356&quot; data-start=&quot;11111&quot;&gt;&lt;b&gt;알고리즘(Algorithm)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744034516277&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function createShareLink(travelId):
    uniqueId = generateUUID()
    dbUpdate(travelId, { shareId: uniqueId, isPublic: true })
    return &quot;https://domain.com/share/&quot; + uniqueId&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;11361&quot; data-start=&quot;11358&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;11389&quot; data-start=&quot;11363&quot; data-ke-size=&quot;size23&quot;&gt;2.7 공유 페이지 열람 (AC7 계열)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;12061&quot; data-start=&quot;11391&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;12061&quot; data-start=&quot;11391&quot;&gt;&lt;b&gt;AC7.1 (공개 페이지 렌더링)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;12061&quot; data-start=&quot;11420&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;11585&quot; data-start=&quot;11420&quot;&gt;&lt;b&gt;정책(Policy)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;11585&quot; data-start=&quot;11444&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;11526&quot; data-start=&quot;11444&quot;&gt;SSR(Next.js의 getServerSideProps 등) 사용 시, 요청된 shareId를 DB에서 조회해 여행기 정보를 불러온다.&lt;/li&gt;
&lt;li data-end=&quot;11585&quot; data-start=&quot;11532&quot;&gt;isPublic이 true인 경우에만 페이지 렌더링. 아니면 &amp;ldquo;존재하지 않는 페이지&amp;rdquo; 안내.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;12061&quot; data-start=&quot;11589&quot;&gt;&lt;b&gt;알고리즘(Algorithm)&lt;/b&gt; (SSR 예시)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744034538541&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function getServerSideProps(context):
    shareId = context.params.shareId
    travelData = dbSelectByShareId(shareId)

    if (!travelData || !travelData.isPublic):
        return { notFound: true }  // 404 페이지

    return {
        props: {
            story: travelData.storyText,
            photos: travelData.photoList,
            ...
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기획 문서를 기반으로 하여 각 인수 조건을 달성하는 형태의 개발을 진행할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 기술적인 부분을 정리하고 문서화하는 내용을 담아보겠습니다.&lt;/p&gt;</description>
      <category>AI로 1인 개발</category>
      <category>1인개발</category>
      <category>AI</category>
      <category>AI개발</category>
      <category>ChatGPT</category>
      <category>COPILOT</category>
      <category>LLM</category>
      <author>스튜디오 폴라리스</author>
      <guid isPermaLink="true">https://polaris-workshop.tistory.com/1</guid>
      <comments>https://polaris-workshop.tistory.com/1#entry1comment</comments>
      <pubDate>Mon, 7 Apr 2025 23:04:51 +0900</pubDate>
    </item>
  </channel>
</rss>