<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Dannian's Warehouse</title>
    <link>https://dragoner.tistory.com/</link>
    <description>블로그에 재미를 붙일 겸, 공부나 업무하면서 알게 된 것 정리 등 이것 저것 올리는 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 29 Jun 2026 02:57:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Dannian</managingEditor>
    <image>
      <title>Dannian's Warehouse</title>
      <url>https://tistory1.daumcdn.net/tistory/833900/attach/ef43fa16d3b24e48af6b07cd49a40ed5</url>
      <link>https://dragoner.tistory.com</link>
    </image>
    <item>
      <title>Personal AI Company 셋업 ⑤: 자동 wakeup 패턴으로 에이전트 파이프라인 직렬화하기</title>
      <link>https://dragoner.tistory.com/277</link>
      <description>&lt;h2&gt;왜 자동 wakeup이 필요한가&lt;/h2&gt;
&lt;p&gt;에이전트가 작업을 완료했는데 다음 단계가 시작되지 않는다면 파이프라인은 조용히 멈춘다. 사람이 대시보드를 계속 보면서 &amp;quot;Researcher 끝났으니까 이제 Creator 깨워야지&amp;quot;를 직접 수행하면 자동화가 아니다. 그것은 사람이 폴링하는 것이다.&lt;/p&gt;
&lt;p&gt;Paperclip은 이 문제를 &lt;strong&gt;children API + tree-hold&lt;/strong&gt; 조합으로 해결한다. CEO가 child 이슈를 올바르게 등록하고 자신을 blocked 상태로 두면, child가 완료될 때 Paperclip이 CEO를 자동으로 깨운다. 이 메커니즘이 제대로 작동하려면 정확한 API 호출 순서와 상태 관리가 필요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;children API가 자동 wakeup의 열쇠다&lt;/h2&gt;
&lt;p&gt;Paperclip에서 이슈를 만드는 방법은 두 가지다. 이 둘은 겉보기에 비슷하지만 자동 wakeup 관점에서 완전히 다르게 동작한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;&lt;code&gt;POST /api/issues/{id}/children&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;POST /api/issues&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;parent_id 자동 연결&lt;/td&gt;
&lt;td&gt;✅ 자동&lt;/td&gt;
&lt;td&gt;❌ 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자동 wakeup 트리거&lt;/td&gt;
&lt;td&gt;✅ child done 시 parent wakeup&lt;/td&gt;
&lt;td&gt;❌ 동작 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조직 트리 표시&lt;/td&gt;
&lt;td&gt;✅ parent 아래 계층 표시&lt;/td&gt;
&lt;td&gt;❌ 독립 이슈로 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;권한 컨텍스트 상속&lt;/td&gt;
&lt;td&gt;✅ parent 컨텍스트 상속&lt;/td&gt;
&lt;td&gt;❌ 독립 컨텍스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tree-hold 연동&lt;/td&gt;
&lt;td&gt;✅ 동작&lt;/td&gt;
&lt;td&gt;❌ 동작 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파이프라인 사용 가능 여부&lt;/td&gt;
&lt;td&gt;✅ 필수 사용&lt;/td&gt;
&lt;td&gt;❌ 사용 금지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;POST /api/issues&lt;/code&gt;로 만든 이슈는 &lt;code&gt;parent_id&lt;/code&gt;가 없다. Paperclip이 해당 이슈의 완료를 누구에게도 알릴 의무가 없으므로 parent는 영원히 blocked 상태에 머문다. children 엔드포인트를 사용해야 &lt;code&gt;parent_id&lt;/code&gt;가 자동으로 설정되고 완료 시 parent 이슈가 wakeup 큐에 올라간다.&lt;/p&gt;
&lt;p&gt;children API 호출 예시는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST https://api.paperclip.sh/api/issues/{parentIssueId}/children \
  -H &amp;quot;Authorization: Bearer $PAPERCLIP_API_KEY&amp;quot; \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#39;{
    &amp;quot;assigneeAgentId&amp;quot;: &amp;quot;{researcherAgentId}&amp;quot;,
    &amp;quot;title&amp;quot;: &amp;quot;[Researcher] 5편 wakeup 패턴 조사&amp;quot;,
    &amp;quot;description&amp;quot;: &amp;quot;... 위임 메시지 전체 ...&amp;quot;
  }&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;tree-hold 패턴: blocked + tree-hold는 한 쌍이다&lt;/h2&gt;
&lt;p&gt;child 이슈를 만든 것만으로는 부족하다. CEO 이슈가 계속 &lt;code&gt;in_progress&lt;/code&gt; 상태라면 다음 heartbeat에서 CEO가 깨어나 다음 작업을 시작할 수 있다. child 완료를 기다리지 않고 Creator child를 먼저 만들어버리는 문제가 발생한다.&lt;/p&gt;
&lt;p&gt;두 가지 API를 순서대로 호출해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 이슈 상태를 blocked로 변경&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X PATCH https://api.paperclip.sh/api/issues/{parentIssueId} \
  -H &amp;quot;Authorization: Bearer $PAPERCLIP_API_KEY&amp;quot; \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#39;{&amp;quot;status&amp;quot;: &amp;quot;blocked&amp;quot;}&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;② tree-hold 등록&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST https://api.paperclip.sh/api/issues/{parentIssueId}/tree-holds \
  -H &amp;quot;Authorization: Bearer $PAPERCLIP_API_KEY&amp;quot; \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#39;{
    &amp;quot;mode&amp;quot;: &amp;quot;pause&amp;quot;,
    &amp;quot;reason&amp;quot;: &amp;quot;waiting for child {childIssueId} (Researcher 완료 대기)&amp;quot;
  }&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mode: &amp;quot;pause&amp;quot;&lt;/code&gt;는 child가 완료되면 tree-hold가 자동으로 해제되는 방식이다. &lt;code&gt;blocked&lt;/code&gt; 상태는 &amp;quot;외부 의존성 때문에 진행 불가&amp;quot;라는 신호이고, tree-hold는 실행 큐에 &amp;quot;내가 풀 때까지 이 이슈를 깨우지 말라&amp;quot;는 잠금이다. 둘 중 하나만 설정해도 예상과 다른 동작이 발생한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;직렬 처리 강제 패턴&lt;/h2&gt;
&lt;h3&gt;동시에 만들면 생기는 문제&lt;/h3&gt;
&lt;p&gt;조사→생성→검증→실행의 4단계 파이프라인이 있다고 가정하자. CEO가 한 번에 4개 child를 만들면 다음 상황이 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CEO → Researcher, Creator, Reviewer, Executor 동시 생성
         ↓           ↓          ↓           ↓
     (리서치 중)  (참조할     (참조할     (참조할
                  리서치 없음) 초안 없음)  검수 없음)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creator는 Researcher의 산출물 경로를 참조해야 하지만 실행 시점에 해당 파일이 존재하지 않는다. 결과는 에러이거나 잘못된 산출물이다. 직렬 처리는 설계 취향의 문제가 아니라 의존성 때문에 반드시 지켜야 하는 제약이다.&lt;/p&gt;
&lt;h3&gt;올바른 직렬 처리 흐름&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CEO 이슈 (in_progress)
  │
  ├─ [Step 1] POST /children → Researcher child 생성
  │     ↓
  │  CEO → blocked + tree-hold 등록
  │     ↓ (Researcher done)
  │  [자동 wakeup] CEO → in_progress 복귀
  │
  ├─ [Step 2] 리서치 산출물 확인 후
  │           POST /children → Creator child 생성
  │     ↓
  │  CEO → blocked + tree-hold 등록
  │     ↓ (Creator done)
  │  [자동 wakeup] CEO → in_progress 복귀
  │
  ├─ [Step 3] 초안 확인 후
  │           POST /children → Reviewer child 생성
  │     ↓
  │  CEO → blocked + tree-hold 등록
  │     ↓ (Reviewer done)
  │  [자동 wakeup] CEO → in_progress 복귀
  │
  └─ [Step 4] 검수 통과 확인 후 CEO → done&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심 원칙은 하나다: &lt;strong&gt;다음 child는 이전 child가 done이 된 후에만 만든다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;CEO 관점 전체 시퀀스&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1단계: 에이전트 ID 조회
curl https://api.paperclip.sh/api/companies/{companyId}/agents \
  -H &amp;quot;Authorization: Bearer $PAPERCLIP_API_KEY&amp;quot;
# → Researcher의 agentId 확인

# 2단계: child 이슈 생성
curl -X POST https://api.paperclip.sh/api/issues/{myCEOIssueId}/children \
  -H &amp;quot;Authorization: Bearer $PAPERCLIP_API_KEY&amp;quot; \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#39;{&amp;quot;assigneeAgentId&amp;quot;: &amp;quot;{researcherAgentId}&amp;quot;, &amp;quot;title&amp;quot;: &amp;quot;...&amp;quot;, &amp;quot;description&amp;quot;: &amp;quot;...&amp;quot;}&amp;#39;
# → childIssueId 저장

# 3단계: 내 이슈를 blocked로
curl -X PATCH https://api.paperclip.sh/api/issues/{myCEOIssueId} \
  -H &amp;quot;Authorization: Bearer $PAPERCLIP_API_KEY&amp;quot; \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#39;{&amp;quot;status&amp;quot;: &amp;quot;blocked&amp;quot;}&amp;#39;

# 4단계: tree-hold 등록 (즉시)
curl -X POST https://api.paperclip.sh/api/issues/{myCEOIssueId}/tree-holds \
  -H &amp;quot;Authorization: Bearer $PAPERCLIP_API_KEY&amp;quot; \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#39;{&amp;quot;mode&amp;quot;: &amp;quot;pause&amp;quot;, &amp;quot;reason&amp;quot;: &amp;quot;waiting for child {childIssueId}&amp;quot;}&amp;#39;

# → Paperclip이 Researcher를 wakeup
# → Researcher done → CEO 자동 wakeup&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;이 시리즈 자체가 작동 증거다&lt;/h2&gt;
&lt;p&gt;이 블로그 시리즈(Personal AI Company 셋업)는 위 패턴을 그대로 적용해 운영한다. 각 편의 작성 흐름은 다음 구조를 따른다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CEO 이슈: 블로그 N편 작성
  │
  ├─ child: [Researcher] N편 조사
  │     → /app/workspace/blog-personal-ai-company-setup/research/{N편}.md
  │     → done → CEO 자동 wakeup
  │
  ├─ child: [Creator] N편 초안 작성
  │     → /app/workspace/blog-personal-ai-company-setup/drafts/{N편}.md
  │     → done → CEO 자동 wakeup
  │
  └─ child: [Reviewer] N편 검수
        → /app/workspace/blog-personal-ai-company-setup/reviews/{N편}-review.md
        → done → CEO 최종 보고&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 편의 리서치 파일이 &lt;code&gt;/app/workspace/&lt;/code&gt; 아래에 실제로 존재하고 Creator인 내가 지금 이 글을 쓰고 있다는 사실 자체가 패턴이 작동했다는 증거다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;에이전트가 스스로 blocked를 선택하는 이유&lt;/h2&gt;
&lt;p&gt;&amp;quot;왜 에이전트가 스스로 blocked 상태로 가는가?&amp;quot;라는 질문이 처음에는 어색하게 느껴질 수 있다. blocked는 보통 &amp;quot;문제가 생긴 상태&amp;quot;로 인식되기 때문이다.&lt;/p&gt;
&lt;p&gt;Paperclip에서 blocked는 &lt;strong&gt;의도적인 대기 신호&lt;/strong&gt;다. &amp;quot;나는 다음 단계를 알고 있지만, 선행 조건이 충족될 때까지 실행하지 않겠다&amp;quot;는 명시적 선언이다. 에이전트가 blocked를 선택하지 않으면 시스템은 에이전트가 계속 작업할 수 있다고 판단하고, 다음 heartbeat에서 불완전한 상태로 진행을 강제한다. blocked + tree-hold는 &amp;quot;올바른 순서를 강제하는 자기 제어 메커니즘&amp;quot;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;트러블슈팅: wakeup 실패 시나리오 3가지&lt;/h2&gt;
&lt;h3&gt;시나리오 1: child를 일반 API로 만든 경우&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;증상&lt;/strong&gt;: Researcher가 done이 됐는데 CEO 이슈가 계속 blocked 상태다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;원인&lt;/strong&gt;: &lt;code&gt;POST /api/issues&lt;/code&gt; 또는 &lt;code&gt;POST /api/companies/{id}/issues&lt;/code&gt;로 child를 만들어서 &lt;code&gt;parent_id&lt;/code&gt;가 없다. Paperclip이 완료를 parent에게 알리지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결책&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Board가 대시보드에서 CEO 이슈 status를 &lt;code&gt;todo&lt;/code&gt; → &lt;code&gt;in_progress&lt;/code&gt;로 수동 변경한다.&lt;/li&gt;
&lt;li&gt;CEO가 깨어난 후 리서치 산출물 경로를 직접 확인한다.&lt;/li&gt;
&lt;li&gt;이후 단계부터 반드시 &lt;code&gt;/api/issues/{id}/children&lt;/code&gt; 엔드포인트를 사용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;시나리오 2: blocked 처리를 잊은 경우&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;증상&lt;/strong&gt;: CEO가 Researcher child를 만들었는데 Researcher 완료 전에 Creator child도 생성됐다. Creator가 리서치 파일 없이 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;원인&lt;/strong&gt;: child 생성 후 blocked 상태로 변경하지 않아 다음 heartbeat에서 CEO가 다음 단계를 진행했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결책&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;잘못 생성된 Creator child 이슈를 &lt;code&gt;cancelled&lt;/code&gt;로 변경한다.&lt;/li&gt;
&lt;li&gt;Researcher가 완료될 때까지 CEO 이슈를 수동으로 blocked 상태로 유지한다.&lt;/li&gt;
&lt;li&gt;Researcher 완료 후 Board가 CEO 이슈를 수동 unblock한다.&lt;/li&gt;
&lt;li&gt;CEO가 리서치 산출물을 확인한 후 Creator child를 재생성한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;시나리오 3: tree-hold 해제 후에도 wakeup이 오지 않는 경우&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;증상&lt;/strong&gt;: child가 done이 됐고 tree-hold도 해제됐는데 CEO 이슈가 &lt;code&gt;in_progress&lt;/code&gt;로 돌아오지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;원인&lt;/strong&gt;: 자동 wakeup 메커니즘의 현재 알려진 제약이다. 버그가 아니며, Paperclip에서 개선 예정 기능이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결책&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Board가 대시보드에서 CEO 이슈와 child 이슈의 상태를 확인한다.&lt;/li&gt;
&lt;li&gt;Child가 실제로 done인지 확인한다.&lt;/li&gt;
&lt;li&gt;CEO 이슈 status를 &lt;code&gt;todo&lt;/code&gt; 또는 &lt;code&gt;in_progress&lt;/code&gt;로 수동 변경한다.&lt;/li&gt;
&lt;li&gt;CEO가 깨어나면 child 산출물 경로를 확인하고 다음 단계를 진행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;현재 운영 중에는 Board(사용자)가 대시보드를 주기적으로 확인해 stuck 이슈를 수동으로 unblock하는 것을 권장한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;패턴 체크리스트&lt;/h2&gt;
&lt;p&gt;파이프라인 구성 시 다음 순서를 반드시 지킨다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;POST /api/issues/{id}/children&lt;/code&gt; 으로 child 생성 (&lt;code&gt;POST /api/issues&lt;/code&gt; 금지)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; child 생성 직후 &lt;code&gt;PATCH /api/issues/{id}&lt;/code&gt; 로 parent를 &lt;code&gt;blocked&lt;/code&gt; 상태로 변경&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; blocked 변경 직후 &lt;code&gt;POST /api/issues/{id}/tree-holds&lt;/code&gt; 로 &lt;code&gt;mode: &amp;quot;pause&amp;quot;&lt;/code&gt; 등록&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 다음 child는 이전 child가 done이 된 후에만 생성&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 자동 wakeup 실패 대비: Board가 대시보드를 주기적으로 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;다음 편 예고&lt;/h2&gt;
&lt;p&gt;6편에서는 Telegram bridge로 AI 에이전트 팀을 모바일에서 원격 제어하는 방법을 다룬다. polling과 webhook의 선택 기준, callback_data 64바이트 제약 해결 패턴, 화이트리스트 기반 접근 제어, Paperclip API 연동 흐름을 설명한다.&lt;/p&gt;</description>
      <category>AI</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/277</guid>
      <comments>https://dragoner.tistory.com/277#entry277comment</comments>
      <pubDate>Thu, 21 May 2026 09:00:52 +0900</pubDate>
    </item>
    <item>
      <title># Personal AI Company 셋업 ④: Path B &amp;mdash; 에이전트 간 파일 공유의 해법</title>
      <link>https://dragoner.tistory.com/276</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Researcher가 조사를 마치고 &quot;저장 완료&quot;를 보고했다. Creator가 해당 파일을 읽으려 했는데 파일이 없다. 에이전트 로그에는 Write 도구 호출이 성공으로 기록되어 있다. 이 시리즈 4편에서는 이 현상의 원인과 해법, 절대경로 규칙의 필요성을 실제 프로젝트 구조를 기반으로 설명한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 인식: 에이전트는 왜 서로의 파일을 못 찾는가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;per-agent sandbox의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Paperclip의 각 에이전트는 독립된 실행 환경(sandbox)에서 작동한다. 에이전트가 실행될 때마다 격리된 git worktree 또는 operator branch가 할당되며, 상대경로(&lt;code&gt;./workspace/&lt;/code&gt;)는 이 격리 디렉토리를 기준으로 resolve된다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;에이전트 A (Researcher) CWD: /paperclip/workspaces/run-20260518-001/
에이전트 B (Creator)    CWD: /paperclip/workspaces/run-20260518-002/

A에서 ./workspace/research/output.md
  = /paperclip/workspaces/run-20260518-001/workspace/research/output.md

B에서 ./workspace/research/output.md
  = /paperclip/workspaces/run-20260518-002/workspace/research/output.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 상대경로가 물리적으로 서로 다른 파일을 가리킨다. Researcher가 상대경로로 저장했다면, Creator는 그 파일을 읽을 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상대경로가 실패하는 두 가지 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트의 Write 도구 호출이 성공으로 기록되어도 다른 에이전트가 파일을 찾지 못하는 경우는 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에이전트가 상대경로를 사용해 자신의 격리 sandbox에 저장했다.&lt;/li&gt;
&lt;li&gt;격리 sandbox는 heartbeat 간 상태 유지를 보장하지 않아, 다음 실행에서 파일이 사라졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 경우 모두 공유 경로 미사용이 근본 원인이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Path B: 공유 워크스페이스 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;host bind-mount란 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/app/workspace/&lt;/code&gt;는 호스트 파일시스템의 특정 디렉토리를 모든 에이전트 컨테이너에 동일하게 마운트한 공유 경로다. 아래는 docker-compose 기반 설정 예시다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose.yml
services:
  paperclip:
    volumes:
      - /host/data/workspace:/app/workspace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 내부에서 &lt;code&gt;/app/workspace/&lt;/code&gt;를 읽거나 쓰면, 그 변경이 호스트 파일시스템(&lt;code&gt;/host/data/workspace/&lt;/code&gt;)에 그대로 반영된다. 어떤 컨테이너에서 쓰든 같은 물리 위치에 기록된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/app/workspace/가 공유 채널이 되는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 에이전트는 동일한 &lt;code&gt;/app/workspace/&lt;/code&gt; 마운트 포인트를 가진다. Researcher가 &lt;code&gt;/app/workspace/blog-project/research/output.md&lt;/code&gt;에 저장하면, Creator는 같은 절대경로로 그 파일을 읽을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;[Researcher sandbox]
/app/workspace/blog/research/output.md
  = /host/data/workspace/blog/research/output.md  &amp;larr; 동일 물리 경로

[Creator sandbox]
/app/workspace/blog/research/output.md
  = /host/data/workspace/blog/research/output.md  &amp;larr; 동일 물리 경로&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트가 어떤 격리 sandbox에서 실행되더라도 &lt;code&gt;/app/workspace/&lt;/code&gt;는 항상 같은 물리 위치를 가리킨다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Path A(sandbox) vs Path B(공유) 비교&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Path A &amp;mdash; per-agent sandbox&lt;/th&gt;
&lt;th&gt;Path B &amp;mdash; 공유 워크스페이스&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;경로 형식&lt;/td&gt;
&lt;td&gt;상대경로 (&lt;code&gt;./workspace/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;절대경로 (&lt;code&gt;/app/workspace/&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;물리 위치&lt;/td&gt;
&lt;td&gt;에이전트마다 다름&lt;/td&gt;
&lt;td&gt;모든 에이전트 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에이전트 간 파일 공유&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heartbeat 간 영속성&lt;/td&gt;
&lt;td&gt;보장 안 됨&lt;/td&gt;
&lt;td&gt;보장됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;td&gt;임시 스크래치&lt;/td&gt;
&lt;td&gt;공식 산출물 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;절대경로 규칙: 5개 에이전트의 공통 약속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5개 에이전트(CEO, Researcher, Creator, Reviewer, Executor)는 각각의 시스템 프롬프트에 동일한 절대경로 규칙 섹션을 가진다. 실제 시스템 프롬프트에서 발췌한 CEO의 규칙은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;## ⚠️ 절대경로 규칙 (최우선)

모든 파일 참조는 호스트와 공유되는 절대경로를 사용한다.
본인의 격리 workspace(상대경로 ./workspace/)는 임시 스크래치 용도로만 쓴다.

| 종류              | 절대경로                                     |
|-------------------|----------------------------------------------|
| 스킬 파일         | /app/skills/{name}.md                        |
| Researcher 산출물 | /app/workspace/{project-name}/research/      |
| Creator 산출물    | /app/workspace/{project-name}/drafts/        |
| Reviewer 산출물   | /app/workspace/{project-name}/reviews/       |
| Executor 최종본   | /app/workspace/{project-name}/final/         |&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CEO가 정의하는 {project-name}&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CEO는 새 프로젝트 시작 시 &lt;code&gt;{project-name}&lt;/code&gt;을 결정하고 위임 메시지에 명시한다. 나머지 에이전트는 CEO가 지정한 프로젝트명을 임의로 변경하지 않는다. 이 규칙 하나가 파이프라인 전체의 경로 일관성을 보장한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할별 읽기/쓰기 경로 매핑&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;CEO:        읽기 &amp;mdash; 모든 하위 디렉토리
Researcher: 쓰기 &amp;mdash; /app/workspace/{project}/research/
            읽기 &amp;mdash; /app/skills/
Creator:    쓰기 &amp;mdash; /app/workspace/{project}/drafts/
            읽기 &amp;mdash; /app/workspace/{project}/research/, /app/skills/
Reviewer:   쓰기 &amp;mdash; /app/workspace/{project}/reviews/
            읽기 &amp;mdash; research/, drafts/, /app/skills/
Executor:   쓰기 &amp;mdash; /app/workspace/{project}/final/
            읽기 &amp;mdash; drafts/, reviews/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 디렉토리 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;research/ &amp;rarr; drafts/ &amp;rarr; reviews/ &amp;rarr; final/ 4단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디렉토리 구조는 파이프라인 단계와 1:1로 대응한다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;/app/workspace/{project-name}/
├── research/    # Researcher 산출물 &amp;rarr; Creator, Reviewer가 읽음
├── drafts/      # Creator 산출물  &amp;rarr; Reviewer, Executor가 읽음
├── reviews/     # Reviewer 산출물 &amp;rarr; Executor가 읽음
└── final/       # Executor 최종본 &amp;rarr; Board 승인 후 외부 발행&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현 블로그 프로젝트 구조 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈를 작성하면서 실제로 생성된 구조다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/app/workspace/blog-personal-ai-company-setup/
│
├── research/
│   ├── 01-why-ai-company.md
│   ├── 02-paperclip-setup.md
│   ├── 03-agent-design.md
│   └── 04-shared-workspace.md
│
├── drafts/
│   ├── 01-why-ai-company.md
│   ├── 02-paperclip-setup.md
│   └── 03-agent-design.md
│
├── reviews/
│   ├── 01-why-ai-company.review.md
│   ├── 02-paperclip-setup.review.md
│   └── 03-agent-design.review.md
│
└── final/
    ├── 01-why-ai-company.md
    ├── 02-paperclip-setup.md
    └── 03-agent-design.md&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CEO의 초기화 책임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CEO는 새 프로젝트를 시작할 때 Researcher에게 위임하기 전에 디렉토리 구조를 먼저 생성해야 한다. 이 단계를 빠뜨리면 Researcher의 첫 번째 저장에서 &lt;code&gt;FileNotFoundError&lt;/code&gt;가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# CEO가 프로젝트 시작 시 실행
mkdir -p /app/workspace/blog-personal-ai-company-setup/{research,drafts,reviews,final}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트러블슈팅: 경로 관련 자주 만나는 오류&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오류 1: 파일 없음 (상대경로 저장 불일치)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;FileNotFoundError: /app/workspace/blog-project/research/output.md not found&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Creator가 Researcher 산출물을 찾지 못한다. Researcher가 완료 보고를 했는데도 파일이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;br /&gt;Researcher가 &lt;code&gt;./workspace/research/output.md&lt;/code&gt;(상대경로)에 저장했다. Creator는 위임 메시지에 적힌 절대경로 &lt;code&gt;/app/workspace/blog-project/research/output.md&lt;/code&gt;를 읽으려 하지만 해당 위치에 파일이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;&lt;br /&gt;Researcher 시스템 프롬프트의 &lt;code&gt;## ⚠️ 절대경로 규칙&lt;/code&gt; 섹션을 점검한다. 파일 존재 여부는 아래 명령으로 직접 확인한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;ls /app/workspace/blog-project/research/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오류 2: 프로젝트 디렉토리 없음 (CEO 초기화 누락)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;FileNotFoundError: [Errno 2] No such file or directory:
'/app/workspace/new-project/research/output.md'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Researcher가 파일 저장을 시도하지만 디렉토리 자체가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;br /&gt;CEO가 새 프로젝트 시작 시 &lt;code&gt;/app/workspace/{project-name}/&lt;/code&gt; 하위 디렉토리를 생성하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;&lt;br /&gt;CEO가 Researcher에게 위임하기 전에 프로젝트 디렉토리 구조를 먼저 생성한다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;mkdir -p /app/workspace/{project-name}/{research,drafts,reviews,final}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오류 3: sandbox 혼동 (성공 보고 후 파일 없음)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;br /&gt;Researcher가 &quot;저장 완료&quot;를 보고했다. 에이전트 로그에는 Write 도구 호출이 성공으로 기록되어 있다. 하지만 &lt;code&gt;/app/workspace/project/research/&lt;/code&gt;를 확인하면 파일이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;br /&gt;에이전트가 상대경로(&lt;code&gt;./workspace/research/output.md&lt;/code&gt;)로 파일을 저장했다. Write 도구 자체는 성공했지만 에이전트의 격리 sandbox에 저장됐다. 격리 sandbox는 공유 경로가 아니므로 다른 에이전트에서 접근할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;&lt;br /&gt;에이전트 heartbeat 로그에서 실제 Write 도구 호출 경로를 확인한다. 경로가 &lt;code&gt;/app/workspace/&lt;/code&gt;로 시작하지 않으면 절대경로 규칙 위반이다. Paperclip UI &amp;rarr; 해당 에이전트 &amp;rarr; Instructions 탭에서 절대경로 규칙 섹션을 점검하고 재저장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오류 4: 프로젝트명 불일치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;br /&gt;&lt;code&gt;/app/workspace/blog-personal-ai-company/research/&lt;/code&gt;에 파일이 있는데 Creator가 &lt;code&gt;/app/workspace/blog-personal-ai-company-setup/research/&lt;/code&gt;를 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;br /&gt;CEO 위임 메시지에 기재한 &lt;code&gt;{project-name}&lt;/code&gt;과 에이전트가 실제로 사용한 디렉토리명이 다르다. 오타, 버전 불일치, 또는 에이전트가 임의로 프로젝트명을 변경했을 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;&lt;br /&gt;CEO가 위임 메시지에서 &lt;code&gt;{project-name}&lt;/code&gt;을 명확히 명시한다. 에이전트는 CEO가 지정한 프로젝트명을 임의로 변경하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 실제 디렉토리 확인
ls /app/workspace/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오류 5: 스킬 파일 읽기 실패&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;br /&gt;에이전트가 스킬 파일 없이 기본 행동으로 작업을 진행한다. 블로그 문체 규칙이나 코드 스타일 가이드가 무시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;br /&gt;스킬 파일 경로가 상대경로이거나 오타가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;&lt;br /&gt;스킬 파일 참조 시 반드시 절대경로를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;올바름: /app/skills/blog-writing.md
잘못됨: ./skills/blog-writing.md
잘못됨: skills/blog-writing.md
잘못됨: ../skills/blog-writing.md&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며: 절대경로 하나로 팀 협업이 성립한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대경로 규칙 하나가 5개 에이전트의 협업을 성립시킨다. 어느 한 에이전트가 이 규칙을 어기면 파이프라인 전체가 깨진다. 시스템 프롬프트에 &lt;code&gt;## ⚠️ 절대경로 규칙 (최우선)&lt;/code&gt;을 명시하고 모든 에이전트에 일관되게 적용하면, Researcher의 산출물이 Creator에게, Creator의 초안이 Reviewer에게, Reviewer의 검증본이 Executor에게 정확하게 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 이 파이프라인의 품질 게이트인 Reviewer 에이전트의 검수 기준과 실제 리뷰 산출물 구조를 살펴본다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>AI 에이전트 파일 공유</category>
      <category>bind-mount</category>
      <category>Paperclip 워크스페이스</category>
      <category>Personal AI Company</category>
      <category>에이전트 sandbox</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/276</guid>
      <comments>https://dragoner.tistory.com/276#entry276comment</comments>
      <pubDate>Wed, 20 May 2026 23:00:15 +0900</pubDate>
    </item>
    <item>
      <title>Personal AI Company 셋업 ③: 5개 에이전트 설계와 등록</title>
      <link>https://dragoner.tistory.com/275</link>
      <description>&lt;h2&gt;팀 구조 설계: 왜 5명인가&lt;/h2&gt;
&lt;p&gt;에이전트를 하나로 만들면 간단해 보이지만 실제로는 역효과이다. 단일 에이전트가 조사·생성·검증·실행을 모두 담당하면 시스템 프롬프트가 비대해지고, 한 역할의 실수가 전체 파이프라인에 영향을 준다.&lt;/p&gt;
&lt;p&gt;Personal AI Company는 &lt;strong&gt;단일 책임 원칙(Single Responsibility)&lt;/strong&gt;을 에이전트 설계에 적용했다. CEO는 조율만 하고, 나머지 4개가 각 단계를 전담한다. 이렇게 하면 에이전트 교체나 재설계가 쉽고, 각 역할의 품질 기준을 독립적으로 높일 수 있다.&lt;/p&gt;
&lt;p&gt;파이프라인 흐름은 다음과 같다: &lt;strong&gt;조사(Researcher) → 생성(Creator) → 검증(Reviewer) → 실행(Executor)&lt;/strong&gt;. CEO는 Board(사용자)의 고수준 지시를 이 4단계로 분해하여 child issue로 위임한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;각 에이전트 비교표&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;에이전트&lt;/th&gt;
&lt;th&gt;Paperclip role&lt;/th&gt;
&lt;th&gt;모델&lt;/th&gt;
&lt;th&gt;주요 역할&lt;/th&gt;
&lt;th&gt;월 예산&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;CEO&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;분석·위임·검수, child issue 생성&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Researcher&lt;/td&gt;
&lt;td&gt;researcher&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;외부 정보 수집, research/ 저장&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creator&lt;/td&gt;
&lt;td&gt;engineer&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;블로그·코드·SNS 초안 생성&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer&lt;/td&gt;
&lt;td&gt;qa&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;스킬 기준 검수, 최대 2회 재검토&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executor&lt;/td&gt;
&lt;td&gt;devops&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Haiku 4.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;승인된 배포·Git 명령 실행&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Executor만 Haiku를 사용한다. 파일 복사, API 호출, Git 명령처럼 단순한 실행 작업에 Sonnet을 쓰는 건 과소비이기 때문이다. 설정 파일에도 이 의도가 주석으로 명시되어 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# executor.yaml
name: executor
model: claude-haiku-4-5-20251001  # 단순 실행 작업 — Haiku로 비용 절감
budget_monthly: 5&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;AGENTS.md로 시스템 프롬프트 관리하기&lt;/h2&gt;
&lt;p&gt;Paperclip은 에이전트 시스템 프롬프트를 &lt;strong&gt;managed bundle&lt;/strong&gt; 방식으로 관리한다. 실제 저장 위치는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/paperclip/instances/default/companies/{companyId}/agents/{agentId}/instructions/AGENTS.md&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AGENTS.md는 Claude Code가 대화 시작 시 자동으로 로드하는 특별 파일이다. Paperclip이 이 파일을 에이전트 시스템 프롬프트의 진입점으로 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;&lt;strong&gt;Append 패턴&lt;/strong&gt;이란 초기 역할 정의 위에 섹션을 점진적으로 추가하는 방식이다. 에이전트를 재시작하지 않아도 지침을 동적으로 수정할 수 있고, 변경 이력도 추적할 수 있다.&lt;/del&gt; &amp;lt;- 이런게 AI가 맘대로 작성한 내용이다. 그렇기 때문에 사용자는 모든걸 곧이곧대로 믿지 말고 항상 의심하고 검수해야만 한다. 그냥 단순히 AGENTS.md 파일 내의 내용을 추가하거나 변경 할 때 매번 Agents들을 재시작 할 필요 없다 라고 이해하면 된다.&lt;/p&gt;
&lt;p&gt;아래는 Creator의 AGENTS.md에서 절대경로 규칙 섹션을 추가한 예시이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;## ⚠️ 절대경로 규칙 (최우선)

모든 파일 참조는 호스트와 공유되는 절대경로를 사용한다.

- 스킬 파일: `/app/skills/{name}.md`
- 산출물 저장: `/app/workspace/{project-name}/drafts/`
- Researcher 자료 참조: `/app/workspace/{project-name}/research/`&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 규칙이 없으면 에이전트가 상대경로로 파일을 참조해 호스트 파일시스템과 연결이 끊어진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;instructionsFilePath 함정 피하기&lt;/h2&gt;
&lt;h3&gt;증상&lt;/h3&gt;
&lt;p&gt;에이전트가 올바른 역할로 동작하지 않거나, 특정 머신에서만 시스템 프롬프트가 로드된다. Paperclip UI에 경고 메시지가 표시되기도 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Using legacy relative instructionsFilePath;
migrate this agent to a managed or absolute external bundle.&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;원인&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;instructionsFilePath&lt;/code&gt;는 &lt;strong&gt;레거시(deprecated) 필드&lt;/strong&gt;이다. 상대경로를 사용하면 &lt;code&gt;adapterConfig.cwd&lt;/code&gt;(에이전트 작업 디렉토리)를 기준으로 경로를 resolve한다. 환경마다 cwd가 다를 수 있어 경로가 불일치한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// agent-instructions.ts (간략화)
function resolveLegacyInstructionsPath(candidatePath: string, config: Record&amp;lt;string, unknown&amp;gt;): string {
  if (path.isAbsolute(candidatePath)) return candidatePath;
  const cwd = asString(config.cwd);
  if (!cwd || !path.isAbsolute(cwd)) {
    throw unprocessable(
      &amp;quot;Legacy relative instructionsFilePath requires adapterConfig.cwd to be set to an absolute path&amp;quot;,
    );
  }
  return path.resolve(cwd, candidatePath);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;해결책&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;권장 여부&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Managed Bundle (Paperclip UI → Instructions 탭 직접 편집)&lt;/td&gt;
&lt;td&gt;✅ 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instructionsRootPath&lt;/code&gt;에 절대경로 지정&lt;/td&gt;
&lt;td&gt;✅ 허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instructionsFilePath&lt;/code&gt;에 절대경로&lt;/td&gt;
&lt;td&gt;⚠️ 동작하나 비권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instructionsFilePath&lt;/code&gt;에 &lt;strong&gt;상대경로&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 금지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;신규 에이전트는 반드시 managed bundle로 시작하고, 기존 에이전트에 상대경로가 있다면 즉시 마이그레이션해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;dangerouslySkipPermissions — 보안 트레이드오프&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;dangerouslySkipPermissions&lt;/code&gt;는 &lt;code&gt;claude-local&lt;/code&gt; 어댑터의 설정 필드로, 기본값은 &lt;code&gt;true&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;Claude Code CLI는 파일 쓰기·터미널 실행 등 위험한 작업 전에 사용자 승인 프롬프트를 표시한다. Paperclip heartbeat는 완전 자동화된 헤드리스 실행이므로 사람이 이 프롬프트에 응답할 수 없다. 프롬프트가 대기 상태가 되면 heartbeat가 타임아웃될 때까지 멈추기 때문에, &lt;code&gt;dangerouslySkipPermissions: true&lt;/code&gt;는 자동화 실행의 구조적 필요조건이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;위험 측면&lt;/strong&gt;은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;에이전트가 시스템의 임의 파일에 접근하거나 수정할 수 있음&lt;/li&gt;
&lt;li&gt;악의적 프롬프트 인젝션 공격 시 완충재 없음&lt;/li&gt;
&lt;li&gt;잘못 설계된 에이전트가 의도치 않은 파일을 덮어쓸 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;완화 조치&lt;/strong&gt;도 함께 작동한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;예산 강제&lt;/strong&gt;: 월 예산 초과 시 에이전트 자동 중단&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;거버넌스&lt;/strong&gt;: Board가 언제든 에이전트를 pause/terminate 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;샌드박스 실행&lt;/strong&gt;: sandbox 환경에서는 &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; 대신 허용 도구 목록(&lt;code&gt;--allowedTools&lt;/code&gt;)으로 대체&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;감사 로그&lt;/strong&gt;: 모든 도구 호출 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// permissions.ts
export function buildClaudeExecutionPermissionArgs(input: {
  dangerouslySkipPermissions: boolean;
  targetIsSandbox: boolean;
}): string[] {
  if (!input.dangerouslySkipPermissions) return [];
  if (input.targetIsSandbox) {
    return [&amp;quot;--allowedTools&amp;quot;, SANDBOX_ALLOWED_TOOLS];  // 샌드박스: 목록 제한
  }
  return [&amp;quot;--dangerously-skip-permissions&amp;quot;];  // 로컬: 전체 허용
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로컬 환경에서 자신이 통제하는 인프라에서 실행하는 경우 합리적인 선택이다. 공용 서버나 신뢰할 수 없는 입력이 있는 환경에서는 반드시 샌드박스 실행을 검토해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Paperclip UI에서 에이전트 등록하기&lt;/h2&gt;
&lt;p&gt;에이전트 등록 절차는 다음 순서로 진행한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Paperclip 대시보드 → Agents 탭&lt;/strong&gt;으로 이동한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New Agent&lt;/strong&gt; 버튼을 클릭하고 이름과 role(researcher, engineer, qa, devops 중 선택)을 입력한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt; 필드에서 모델을 선택한다. Executor는 &lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;, 나머지는 &lt;code&gt;claude-sonnet-4-6&lt;/code&gt;을 지정한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Budget&lt;/strong&gt; 필드에 월 예산 한도(USD)를 입력한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instructions 탭&lt;/strong&gt;으로 이동해 시스템 프롬프트를 편집한다. 여기서 편집한 내용이 managed bundle(AGENTS.md)로 저장된다.&lt;/li&gt;
&lt;li&gt;절대경로 규칙(&lt;code&gt;/app/workspace/{project-name}/&lt;/code&gt;)을 Instructions 최상단에 명시한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;를 클릭하면 다음 heartbeat부터 새 Instructions가 적용된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;instructionsFilePath&lt;/code&gt;나 &lt;code&gt;instructionsRootPath&lt;/code&gt;를 별도로 설정하지 않아도 되며, UI에서 직접 편집하는 managed bundle이 기본값이자 권장 방식이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 편 예고&lt;/h2&gt;
&lt;p&gt;4편에서는 CEO가 child issue를 생성하고 blocked 상태를 관리하는 &lt;strong&gt;워크플로우 자동화&lt;/strong&gt;를 다룬다. &lt;code&gt;POST /api/issues/{id}/children&lt;/code&gt; 패턴과 tree-hold 메커니즘을 실전 예시로 살펴본다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>AGENTS.md</category>
      <category>AI 에이전트</category>
      <category>claude code</category>
      <category>paperclip</category>
      <category>역할 분리</category>
      <category>자율 에이전트</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/275</guid>
      <comments>https://dragoner.tistory.com/275#entry275comment</comments>
      <pubDate>Mon, 18 May 2026 21:00:09 +0900</pubDate>
    </item>
    <item>
      <title>Personal AI Company 셋업 ②: Paperclip + Claude Code Max 셋업</title>
      <link>https://dragoner.tistory.com/274</link>
      <description>&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;Personal AI Company 시리즈 1편에서 전체 아키텍처와 개념을 살펴봤다. 2편에서는 Paperclip을 실제로 설치하고 Claude Code Max와 연결하는 구체적인 절차를 다룬다. 이 글을 따라하면 로컬 또는 서버 환경에서 Paperclip API 서버가 실행되고, Claude Code가 에이전트로 동작하는 상태가 된다.&lt;/p&gt;
&lt;h2&gt;사전 요구사항&lt;/h2&gt;
&lt;h3&gt;필수 항목 확인&lt;/h3&gt;
&lt;p&gt;아래 항목이 모두 준비되어 있어야 한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;버전 / 조건&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;20 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pnpm&lt;/td&gt;
&lt;td&gt;9.15 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude 구독&lt;/td&gt;
&lt;td&gt;Max 플랜 (Claude Code 사용 필수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 체제&lt;/td&gt;
&lt;td&gt;macOS, Linux, Windows (WSL2)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Claude Max 구독이 필요한 이유&lt;/h3&gt;
&lt;p&gt;Paperclip은 Claude Code를 에이전트 런타임으로 사용한다. Claude Code는 Claude Max 플랜 이상에서 높은 사용량 한도로 에이전트 루프 실행이 가능하다. 구독 없이 시작하면 에이전트 실행이 즉시 제한되므로 시작 전에 반드시 확인한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Claude Max 구독 가격은 Anthropic 공식 사이트에서 확인한다 (공식 문서 없음, 추가 확인 필요).&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;Node.js와 pnpm 설치 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node --version   # 20.x 이상인지 확인
pnpm --version   # 9.15 이상인지 확인

# pnpm이 설치되어 있지 않다면
npm install -g pnpm&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Paperclip 설치&lt;/h2&gt;
&lt;h3&gt;npx 원커맨드 설치 (권장)&lt;/h3&gt;
&lt;p&gt;가장 빠른 설치 방법이다. 아래 한 줄로 Paperclip 설치와 초기 설정이 완료된다. 내장 PostgreSQL이 자동 생성되어 별도 DB 설정이 불필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx paperclipai onboard --yes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이미 설정이 있는 경우 &lt;code&gt;onboard&lt;/code&gt;를 재실행해도 기존 설정을 유지한다. 설정 변경이 필요한 경우 &lt;code&gt;paperclipai configure&lt;/code&gt; 명령을 사용한다.&lt;/p&gt;
&lt;h3&gt;bind 모드 선택&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;--bind&lt;/code&gt; 옵션으로 서버의 접근 범위를 설정한다. 처음부터 자신의 용도에 맞는 모드를 선택하는 것이 중요하다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모드&lt;/th&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;접근 범위&lt;/th&gt;
&lt;th&gt;적합한 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;로컬 (기본)&lt;/td&gt;
&lt;td&gt;옵션 생략&lt;/td&gt;
&lt;td&gt;localhost만&lt;/td&gt;
&lt;td&gt;개발·테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LAN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--bind lan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;같은 Wi-Fi 내 기기&lt;/td&gt;
&lt;td&gt;팀 내부 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailscale&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--bind tailnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tailscale 망 전체&lt;/td&gt;
&lt;td&gt;원격·스마트폰 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# LAN 접근 허용 (같은 네트워크의 다른 기기에서 접근)
npx paperclipai onboard --yes --bind lan

# Tailscale 망 내 접근 (원격 접근, 스마트폰 관리용)
npx paperclipai onboard --yes --bind tailnet&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;수동 설치 (개발자용)&lt;/h3&gt;
&lt;p&gt;소스 코드를 직접 수정하거나 커스텀 환경을 구성할 때 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/paperclipai/paperclip.git
cd paperclip
pnpm install
pnpm dev&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버가 시작되면 API 서버는 &lt;code&gt;http://localhost:3100&lt;/code&gt;에서 동작한다.&lt;/p&gt;
&lt;h2&gt;환경변수 설정&lt;/h2&gt;
&lt;p&gt;수동 설치의 경우 또는 기존 설정을 수정할 때 &lt;code&gt;.env&lt;/code&gt; 파일을 직접 편집한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp .env.example .env&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주요 환경변수는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dotenv&quot;&gt;# PostgreSQL 연결 문자열
DATABASE_URL=postgres://paperclip:paperclip@localhost:5432/paperclip

# API 서버 포트 (기본: 3100)
PORT=3100

# UI 서빙 여부 (별도 프론트엔드 서버 사용 시 false)
SERVE_UI=false

# 인증 시크릿 키 ⚠️ 프로덕션에서 반드시 변경
BETTER_AUTH_SECRET=paperclip-dev-secret&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;BETTER_AUTH_SECRET 설정&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BETTER_AUTH_SECRET&lt;/code&gt;은 세션 인증에 사용하는 핵심 보안 키다. 기본값 &lt;code&gt;paperclip-dev-secret&lt;/code&gt;은 개발 전용이며, 외부 접근이 가능한 환경에서는 반드시 강력한 난수로 교체해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS / Linux
openssl rand -base64 32

# Node.js
node -e &amp;quot;console.log(require(&amp;#39;crypto&amp;#39;).randomBytes(32).toString(&amp;#39;base64&amp;#39;))&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;생성된 값을 &lt;code&gt;.env&lt;/code&gt; 파일의 &lt;code&gt;BETTER_AUTH_SECRET&lt;/code&gt;에 입력한다. 이 값이 유출되면 세션 위조가 가능하므로 Git에 커밋하지 않도록 &lt;code&gt;.gitignore&lt;/code&gt;에 &lt;code&gt;.env&lt;/code&gt;가 포함되어 있는지 반드시 확인한다. 값을 변경하면 모든 활성 세션이 무효화된다.&lt;/p&gt;
&lt;h2&gt;Claude CLI 설치 및 인증&lt;/h2&gt;
&lt;h3&gt;Claude CLI 설치&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g @anthropic-ai/claude-code

# 설치 확인
claude --version&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;OAuth 인증 흐름&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Claude CLI 로그인 (브라우저 OAuth 팝업)
claude auth login&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 명령을 실행하면 브라우저가 열리고 Anthropic 계정 로그인 페이지로 이동한다. Claude Max 플랜이 연결된 계정으로 로그인한다. 인증이 완료되면 credentials가 &lt;code&gt;~/.claude/&lt;/code&gt; 디렉토리에 저장된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 인증 상태 확인
claude auth status&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;컨테이너 환경에서 credentials 전달&lt;/h3&gt;
&lt;p&gt;Docker 등 컨테이너 환경에서는 호스트의 &lt;code&gt;~/.claude/&lt;/code&gt; 디렉토리를 컨테이너 내부에 전달해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;아래 내용은 공식 README에 Docker 가이드가 없으므로, 실제 운영 환경에서 관찰된 패턴 기반이다 (공식 문서 없음, 추가 확인 필요).&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;방법 1: Volume 마운트 (권장)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# docker-compose.yml
services:
  paperclip:
    volumes:
      - ~/.claude:/root/.claude:ro  # 읽기 전용 마운트&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;방법 2: 직접 복사&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 호스트에서 컨테이너로 복사
docker cp ~/.claude/. &amp;lt;container_id&amp;gt;:/root/.claude/

# 또는 컨테이너 내부에서 직접 인증
docker exec -it &amp;lt;container_id&amp;gt; claude auth login&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;트러블슈팅&lt;/h2&gt;
&lt;h3&gt;T1. Node.js 버전 오류로 설치 실패&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;증상&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Error: The engine &amp;quot;node&amp;quot; is incompatible with this module&lt;/code&gt; 오류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;원인&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Node.js 버전이 20 미만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;해결책&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvm install 20 &amp;amp;&amp;amp; nvm use 20&lt;/code&gt; 실행 또는 공식 Node.js 사이트에서 최신 LTS를 설치한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;T2. BETTER_AUTH_SECRET 미설정으로 인증 실패&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;증상&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;로그인 시도 시 세션이 즉시 만료되거나 &amp;quot;Invalid secret&amp;quot; 오류가 발생한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;원인&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.env&lt;/code&gt;에 &lt;code&gt;BETTER_AUTH_SECRET&lt;/code&gt;이 없거나 기본값(&lt;code&gt;paperclip-dev-secret&lt;/code&gt;) 상태로 프로덕션 모드 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;해결책&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openssl rand -base64 32&lt;/code&gt;로 새 시크릿 생성 후 &lt;code&gt;.env&lt;/code&gt;에 입력하고 서버를 재시작한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;T3. Claude 인증 후에도 에이전트가 &amp;quot;인증 오류&amp;quot; 반환&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;증상&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude auth status&lt;/code&gt;는 정상인데, Paperclip 에이전트 실행 시 &amp;quot;Authentication required&amp;quot; 오류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;원인&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;컨테이너·격리 환경에서 &lt;code&gt;~/.claude&lt;/code&gt; credentials를 찾지 못함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;해결책&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;호스트의 &lt;code&gt;~/.claude&lt;/code&gt;를 컨테이너 내부로 마운트하거나, 컨테이너 내부에서 직접 &lt;code&gt;claude auth login&lt;/code&gt;을 실행한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;T4. 포트 3100 충돌로 서버 시작 실패&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;증상&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Error: listen EADDRINUSE: address already in use :::3100&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;원인&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;다른 프로세스가 이미 3100 포트를 사용 중&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;해결책&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.env&lt;/code&gt;에서 &lt;code&gt;PORT=3101&lt;/code&gt;(또는 다른 빈 포트)로 변경 후 재시작. &lt;code&gt;lsof -i :3100&lt;/code&gt;으로 충돌 프로세스를 먼저 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;T5. Tailscale bind 모드에서 외부 접근 불가&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;증상&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--bind tailnet&lt;/code&gt;으로 시작했는데 외부 기기에서 대시보드에 접근할 수 없다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;원인&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailscale이 설치되어 있지 않거나 호스트가 Tailscale 망에 미연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;해결책&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailscale 설치 및 &lt;code&gt;tailscale up&lt;/code&gt;으로 로그인 후 Paperclip을 재시작한다. 접근 시 Tailscale IP를 사용한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;다음 편 예고&lt;/h2&gt;
&lt;p&gt;3편에서는 Paperclip 위에서 동작하는 첫 에이전트를 직접 정의하고, CEO-Researcher-Creator 역할 분리 구조를 실제로 구성하는 방법을 다룬다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>AI 에이전트 설치</category>
      <category>BETTER_AUTH_SECRET</category>
      <category>Claude Code Max</category>
      <category>Claude OAuth</category>
      <category>npx onboard</category>
      <category>Paperclip AI</category>
      <category>Personal AI Company</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/274</guid>
      <comments>https://dragoner.tistory.com/274#entry274comment</comments>
      <pubDate>Mon, 18 May 2026 18:00:59 +0900</pubDate>
    </item>
    <item>
      <title>Personal AI Company 셋업 ①: 왜 1인 AI 회사인가</title>
      <link>https://dragoner.tistory.com/273</link>
      <description>&lt;p&gt;AI 에이전트를 여러 개 동시에 운영하다 보면 터미널 탭이 20개를 넘어가고, 어느 에이전트가 무엇을 하고 있는지 파악하기 어려워진다. 1인 AI 회사(Personal AI Company)는 이 문제를 &amp;quot;에이전트를 도구로 쓰지 말고 회사처럼 운영한다&amp;quot;는 관점으로 해결한다. Paperclip은 그 운영 인프라를 제공하는 오픈소스 오케스트레이션 플랫폼이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 문제: AI 에이전트를 많이 쓸수록 복잡해진다&lt;/h2&gt;
&lt;h3&gt;1-1. 터미널 탭 20개의 혼돈&lt;/h3&gt;
&lt;p&gt;Claude Code, Cursor, Copilot 등 AI 에이전트를 적극적으로 활용하기 시작하면 곧 관리 문제에 부딪힌다. 에이전트 하나에 터미널 세션 하나씩 붙여두면, 어느 시점부터는 어느 에이전트가 어느 작업을 담당하는지 직접 추적해야 한다. 재시작하면 컨텍스트가 모두 사라지고, 처음부터 다시 설명해야 한다.&lt;/p&gt;
&lt;h3&gt;1-2. 조율과 비용 통제가 없다&lt;/h3&gt;
&lt;p&gt;단일 에이전트에게 조사, 생성, 검증, 배포를 모두 맡기면 품질이 들쭉날쭉해진다. pay-as-you-go API 방식이라면 토큰 루프 하나로 예상치 못한 청구서가 날아오기도 한다. 에이전트가 &amp;quot;왜&amp;quot; 이 작업을 해야 하는지 모르기 때문에 주어진 지시의 경계를 벗어나기도 한다.&lt;/p&gt;
&lt;p&gt;핵심 문제는 AI 모델 성능이 아니다. 에이전트를 어떻게 조율하느냐가 핵심이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 해결책: 직원이 아닌 회사&lt;/h2&gt;
&lt;h3&gt;2-1. Paperclip이란&lt;/h3&gt;
&lt;p&gt;Paperclip은 AI 에이전트를 회사 수준으로 운영하는 오픈소스 오케스트레이션 플랫폼이다(MIT 라이선스). 공식 문서의 표현을 빌리면 &amp;quot;Claude Code가 직원이라면, Paperclip은 그 회사다.&amp;quot; 조직도, 예산, 거버넌스, 티켓 시스템, 목표 계층을 갖추고 있어 에이전트가 항상 자신이 무엇을 왜 해야 하는지 알고 실행한다.&lt;/p&gt;
&lt;h3&gt;2-2. 핵심 구조&lt;/h3&gt;
&lt;p&gt;Paperclip은 다음 메커니즘으로 에이전트 혼돈을 해결한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;티켓 기반 작업 추적&lt;/strong&gt;: 모든 작업이 이슈로 관리되며 에이전트가 checkout하여 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistent agent state&lt;/strong&gt;: Heartbeat(주기적 wakeup) 사이에도 작업 컨텍스트가 유지된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;목표 계층(Goal Tree)&lt;/strong&gt;: 작업이 상위 목표와 연결되어 에이전트가 &amp;quot;왜&amp;quot;를 항상 파악한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;에이전트별 예산&lt;/strong&gt;: 월 예산 초과 시 자동 일시정지로 비용 폭주를 막는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Governance with rollback&lt;/strong&gt;: 승인 게이트와 설정 버전 관리로 변경을 통제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;로컬 설치는 Node.js 20+, pnpm 9.15+만 있으면 된다(공식 README 기준).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx paperclipai onboard --yes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 명령 한 줄로 로컬 서버와 내장 PostgreSQL이 구동된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 5개 에이전트 조직도&lt;/h2&gt;
&lt;p&gt;Personal AI Company의 기본 구성은 5가지 역할로 이루어진다. 각 역할은 독립된 시스템 프롬프트로 정의되며, Paperclip의 Org Chart 기능이 상하 보고 구조를 관리한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;담당&lt;/th&gt;
&lt;th&gt;역할 설명&lt;/th&gt;
&lt;th&gt;주요 산출물&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CEO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CEO&lt;/td&gt;
&lt;td&gt;Board(사용자)로부터 지시를 받아 작업을 분해·위임·검수한다. 직접 산출물을 만들지 않는다.&lt;/td&gt;
&lt;td&gt;child issue, 작업 분해&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Researcher&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Researcher&lt;/td&gt;
&lt;td&gt;조사 목표와 범위를 확인하고 정보를 수집·구조화한다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/research/*.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Creator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Creator&lt;/td&gt;
&lt;td&gt;Researcher 자료를 바탕으로 콘텐츠·코드를 생성한다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/drafts/*.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reviewer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reviewer&lt;/td&gt;
&lt;td&gt;Creator 산출물을 스킬 파일 기준으로 검증하고 통과/수정 판정을 내린다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/reviews/*.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Executor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Executor&lt;/td&gt;
&lt;td&gt;Board 승인 후 최종 산출물을 외부에 배포한다(블로그 발행, git push 등).&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/final/*.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;워크플로우 방향은 &lt;strong&gt;Board → CEO → Researcher → Creator → Reviewer → Executor&lt;/strong&gt; 순서다. 각 단계는 child issue로 직렬 위임되며, 이전 단계가 완료되어야 다음 단계가 시작된다. 파일시스템은 역할별 디렉토리로 분리되어 에이전트 간 산출물 충돌을 방지한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 비용 구조: Claude Max + Paperclip OSS&lt;/h2&gt;
&lt;h3&gt;4-1. Claude Max 구독 정책&lt;/h3&gt;
&lt;p&gt;Paperclip은 Claude Code를 에이전트 런타임으로 사용한다. 비용은 Anthropic의 Claude Max 구독으로 처리한다(공식 페이지 기준).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;플랜&lt;/th&gt;
&lt;th&gt;월 구독료&lt;/th&gt;
&lt;th&gt;용량&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Claude Pro&lt;/td&gt;
&lt;td&gt;$20/월&lt;/td&gt;
&lt;td&gt;기준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Max (기본)&lt;/td&gt;
&lt;td&gt;$100/월&lt;/td&gt;
&lt;td&gt;Pro의 5배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Max (고용량)&lt;/td&gt;
&lt;td&gt;$200/월&lt;/td&gt;
&lt;td&gt;Pro의 20배&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;4-2. 토큰 추가 과금이 없는 의미&lt;/h3&gt;
&lt;p&gt;API 키 기반 pay-as-you-go 방식과 달리, Claude Max는 구독 고정비만으로 에이전트를 24시간 운영해도 추가 청구가 없다. 1인 운영자 입장에서 월 비용을 $100~$200로 고정할 수 있다는 것은 비용 예측이 단순해진다는 의미다. Paperclip의 에이전트별 예산 기능과 결합하면 각 에이전트의 사용량을 더 세밀하게 통제할 수 있다.&lt;/p&gt;
&lt;p&gt;Paperclip 자체는 MIT 오픈소스이므로 별도 라이선스 비용이 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 실전 시나리오&lt;/h2&gt;
&lt;h3&gt;시나리오 A: 블로그 자동화 파이프라인&lt;/h3&gt;
&lt;p&gt;이 글 자체가 해당 파이프라인으로 작성됐다. (물론 AI가 작성한 모든걸 믿지 않기 때문에 전체 내용을 리뷰 한 상태이다.)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Board가 Paperclip에 이슈 등록: &amp;quot;Personal AI Company 셋업 시리즈 1편 작성&amp;quot;&lt;/li&gt;
&lt;li&gt;CEO가 작업을 분해하고 Researcher child issue를 생성한다.&lt;/li&gt;
&lt;li&gt;Researcher가 참조 파일을 읽고 리서치 문서를 &lt;code&gt;/research/&lt;/code&gt; 에 저장한다.&lt;/li&gt;
&lt;li&gt;Creator가 리서치 자료 기반으로 블로그 초안을 &lt;code&gt;/drafts/&lt;/code&gt; 에 작성한다.&lt;/li&gt;
&lt;li&gt;Reviewer가 스킬 파일 기준으로 검수(맞춤법, SEO, 코드 정확성)한다.&lt;/li&gt;
&lt;li&gt;Board 승인 후 Executor가 GitHub Pages 또는 Tistory에 발행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;시나리오 B: Swift 코드 생성 파이프라인&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Board: &amp;quot;VIPER 패턴으로 로그인 화면 구현&amp;quot;&lt;/li&gt;
&lt;li&gt;CEO가 Creator에게 직행 위임한다(사전 조사가 불필요한 경우).&lt;/li&gt;
&lt;li&gt;Creator가 &lt;code&gt;/app/skills/swift-coding.md&lt;/code&gt; 규칙에 따라 SnapKit + async/await 코드를 생성한다.&lt;/li&gt;
&lt;li&gt;Reviewer가 컴파일 가능 여부, VIPER 레이어 분리, 네이밍 컨벤션을 검수한다.&lt;/li&gt;
&lt;li&gt;Executor가 git commit하고 PR을 생성한다(main 브랜치 push는 Board 승인 필수).&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;시나리오 C: 정기 리포트 자동화&lt;/h3&gt;
&lt;p&gt;Paperclip의 Routines(스케줄) 기능을 활용하면 매주 월요일 오전 9시 같은 트리거를 설정할 수 있다. 트리거 시 Paperclip이 자동으로 이슈를 생성하고 에이전트를 깨운다(Heartbeat). Researcher가 지난 주 작업 현황을 수집하고, Creator가 요약 리포트를 작성한 뒤, Executor가 Telegram이나 이메일로 발송한다. 사람의 개입 없이 정기 보고가 자동화된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 이 시리즈에서 다루는 내용&lt;/h2&gt;
&lt;p&gt;이 시리즈는 Personal AI Company를 직접 구축하는 과정을 순서대로 기록한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1편 (이 글)&lt;/strong&gt;: Personal AI Company 셋업 ①: 왜 1인 AI 회사인가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2편&lt;/strong&gt;: Personal AI Company 셋업 ②: Paperclip + Claude Code Max 셋업&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3편&lt;/strong&gt;: Personal AI Company 셋업 ③: 5개 에이전트 설계와 등록&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4편&lt;/strong&gt;: Personal AI Company 셋업 ④: Path B — 에이전트 간 파일 공유의 해법&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5편&lt;/strong&gt;: Personal AI Company 셋업 ⑤: 자동 wakeup 패턴으로 에이전트 파이프라인 직렬화하기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;6편&lt;/strong&gt;: Personal AI Company 셋업 ⑥: Telegram bridge로 어디서든 에이전트 제어하기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7편&lt;/strong&gt;: Personal AI Company 셋업 ⑦: 개발자 메모 — 자잘한 함정들&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;다음 편 예고&lt;/strong&gt;: 2편에서는 &lt;code&gt;npx paperclipai onboard --yes&lt;/code&gt; 한 줄부터 시작해 Org Chart와 5개 에이전트를 실제로 등록하는 과정을 다룬다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>1인AI회사</category>
      <category>ai에이전트</category>
      <category>ClaudeMax</category>
      <category>paperclip</category>
      <category>에이전트자동화</category>
      <category>오케스트레이션</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/273</guid>
      <comments>https://dragoner.tistory.com/273#entry273comment</comments>
      <pubDate>Mon, 18 May 2026 11:56:19 +0900</pubDate>
    </item>
    <item>
      <title>[Codewars] [7Kyu] V A P O R C O D E</title>
      <link>https://dragoner.tistory.com/267</link>
      <description>&lt;figure id=&quot;og_1732949092045&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Codewars - Achieve mastery through coding practice and developer mentorship&quot; data-og-description=&quot;A coding practice website for all programming levels &amp;ndash; Join a community of over 3 million developers and improve your coding skills in over 55 programming languages!&quot; data-og-host=&quot;www.codewars.com&quot; data-og-source-url=&quot;https://www.codewars.com/kata/5966eeb31b229e44eb00007a/swift&quot; data-og-url=&quot;https://www.codewars.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c50bE2/hyXGFJJUQb/t5pkDAUmVXdIKGA4aPuFs0/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670,https://scrap.kakaocdn.net/dn/b8tf5w/hyXDe76YCQ/ypaJuCQvWqF8faIqQATEq1/img.png?width=1200&amp;amp;height=630&amp;amp;face=271_208_422_374&quot;&gt;&lt;a href=&quot;https://www.codewars.com/kata/5966eeb31b229e44eb00007a/swift&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.codewars.com/kata/5966eeb31b229e44eb00007a/swift&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c50bE2/hyXGFJJUQb/t5pkDAUmVXdIKGA4aPuFs0/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670,https://scrap.kakaocdn.net/dn/b8tf5w/hyXDe76YCQ/ypaJuCQvWqF8faIqQATEq1/img.png?width=1200&amp;amp;height=630&amp;amp;face=271_208_422_374');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Codewars - Achieve mastery through coding practice and developer mentorship&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A coding practice website for all programming levels &amp;ndash; Join a community of over 3 million developers and improve your coding skills in over 55 programming languages!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.codewars.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;1. 받은 문자열의 각 문자를 대문자로 변환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 각 문자 사이 간격 스페이스2개로 구분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 원 문자의 간격은 무시&lt;/p&gt;
&lt;pre id=&quot;code_1732950407753&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func vaporcode(_ str: String) -&amp;gt; String {
    var strArr = str.compactMap({ $0.uppercased() }) // split(separator: &quot;&quot;)
    strArr.removeAll(where: { $0 == &quot; &quot; })
    let returnValue = strArr.joined(separator: &quot;  &quot;)
    return returnValue
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 어려울게 딱히 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 조건대로 사용해주면 되고, 간략하게 적느냐 정도의 차이만 있네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Codewars(알고리즘)/7Kyu</category>
      <category>7kyu</category>
      <category>codewars</category>
      <category>Solution</category>
      <category>SWIFT</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/267</guid>
      <comments>https://dragoner.tistory.com/267#entry267comment</comments>
      <pubDate>Sat, 30 Nov 2024 16:07:38 +0900</pubDate>
    </item>
    <item>
      <title>[Codewars] [5Kyu] Consecutive k-Primes</title>
      <link>https://dragoner.tistory.com/266</link>
      <description>&lt;figure id=&quot;og_1732944066184&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Codewars - Achieve mastery through coding practice and developer mentorship&quot; data-og-description=&quot;A coding practice website for all programming levels &amp;ndash; Join a community of over 3 million developers and improve your coding skills in over 55 programming languages!&quot; data-og-host=&quot;www.codewars.com&quot; data-og-source-url=&quot;https://www.codewars.com/kata/573182c405d14db0da00064e/train/swift&quot; data-og-url=&quot;https://www.codewars.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5vykM/hyXGBHkyJE/e3qrCxEMWZhshQ4ARoLvm1/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670,https://scrap.kakaocdn.net/dn/dbAJDZ/hyXGKEgDsN/Awq2okTzQ065Ru83pNC4t1/img.png?width=1200&amp;amp;height=630&amp;amp;face=271_208_422_374&quot;&gt;&lt;a href=&quot;https://www.codewars.com/kata/573182c405d14db0da00064e/train/swift&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.codewars.com/kata/573182c405d14db0da00064e/train/swift&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5vykM/hyXGBHkyJE/e3qrCxEMWZhshQ4ARoLvm1/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670,https://scrap.kakaocdn.net/dn/dbAJDZ/hyXGKEgDsN/Awq2okTzQ065Ru83pNC4t1/img.png?width=1200&amp;amp;height=630&amp;amp;face=271_208_422_374');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Codewars - Achieve mastery through coding practice and developer mentorship&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A coding practice website for all programming levels &amp;ndash; Join a community of over 3 million developers and improve your coding skills in over 55 programming languages!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.codewars.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 소인수분해를 해서 나온 소수들의 갯수를 확인하고, k 값과 동일한 소수의 갯수가 연속되는 횟수에 대해서 구하는 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Swift&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1-1. 본인의 풀이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소인수분해를 하는 함수를 하나 만들고(primeCountChecker) 해당 함수에서 반환하는 값을 비교해서 k와 동일한 값이 연속해서 반환되는 횟수를 구했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732948423740&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func consecKprimes(_ k: Int, _ arr: [Int]) -&amp;gt; Int {
    // your code
  var returnValue: Int = 0
  var isContinue: Bool = false
  for element in arr {
    let test = primeCountChecker(element)
    guard test == k else {
      if isContinue { 
        isContinue = false
      }
      continue
    }
    if isContinue == false {
      isContinue = true 
    } else {
      returnValue += 1
    }
    
  }
  return returnValue
}

func primeCountChecker(_ n: Int) -&amp;gt; Int {
  var num = n
  var result: [Int] = []
  var count = 2
  while num != 1 {
    if num % count == 0 {
      num = num / count
      result.append(count)
      count = 2
    } else {
      count += 1
    }
  }
  return result.count
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;primeCountChecker의 동작은 나머지가 0인 값이 나오면 해당 값으로 나눈 결과물을 num에 저장하고, 그 값을 다시 2부터 차례로 나누게 하는 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 함수에서 result 배열에는 소수들이 들어있습니다.&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;b&gt;1-2. Best Solution&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Best Practices를 받은 문제풀이입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732948450282&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func consecKprimes(_ k: Int, _ arr: [Int]) -&amp;gt; Int {
    
    func prim(_ n: Int) -&amp;gt; Int {
        var na = n, i = 2, cn = 0
        while na &amp;gt; 1 { while na % i == 0 { cn += 1; na /= i }; i += 1 }
        return cn
    }
    
    var i = 0, cn = 0
    while i &amp;lt; arr.count - 1 {
        if (prim(arr[i]) == k) &amp;amp;&amp;amp; (prim(arr[i + 1]) == k)
        {cn += 1}
        i += 1;
    }
    
    return cn
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작의 원리 자체는 비슷하지만, 훨씬 간략하게 작성했네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘을 풀다보면 언제나 간략하게 작성하는 사람들을 보게되는데, 항상 신기하게 느껴집니다..&lt;/p&gt;</description>
      <category>Codewars(알고리즘)/5Kyu</category>
      <category>5kyu</category>
      <category>codewars</category>
      <category>Solution</category>
      <category>SWIFT</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/266</guid>
      <comments>https://dragoner.tistory.com/266#entry266comment</comments>
      <pubDate>Sat, 30 Nov 2024 15:40:41 +0900</pubDate>
    </item>
    <item>
      <title>[Codewars] [8Kyu] Convert a String to a Number!</title>
      <link>https://dragoner.tistory.com/264</link>
      <description>&lt;figure id=&quot;og_1732941169449&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Codewars - Achieve mastery through coding practice and developer mentorship&quot; data-og-description=&quot;A coding practice website for all programming levels &amp;ndash; Join a community of over 3 million developers and improve your coding skills in over 55 programming languages!&quot; data-og-host=&quot;www.codewars.com&quot; data-og-source-url=&quot;https://www.codewars.com/kata/544675c6f971f7399a000e79/train/swift&quot; data-og-url=&quot;https://www.codewars.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/9efeT/hyXDfzaZnp/5EFiYHuSeNAAAivoUXFFD1/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670,https://scrap.kakaocdn.net/dn/mqolX/hyXDc3n8ih/kDV4pfe0MF4QsakWWF2cjk/img.png?width=1200&amp;amp;height=630&amp;amp;face=271_208_422_374&quot;&gt;&lt;a href=&quot;https://www.codewars.com/kata/544675c6f971f7399a000e79/train/swift&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.codewars.com/kata/544675c6f971f7399a000e79/train/swift&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/9efeT/hyXDfzaZnp/5EFiYHuSeNAAAivoUXFFD1/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670,https://scrap.kakaocdn.net/dn/mqolX/hyXDc3n8ih/kDV4pfe0MF4QsakWWF2cjk/img.png?width=1200&amp;amp;height=630&amp;amp;face=271_208_422_374');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Codewars - Achieve mastery through coding practice and developer mentorship&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A coding practice website for all programming levels &amp;ndash; Join a community of over 3 million developers and improve your coding skills in over 55 programming languages!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.codewars.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 매우 간단한 String -&amp;gt; Int 변경 관련 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 조건에 적혀있는대로면 String은 무조건 들어오고, 그 값 또한 무조건 Int라고 하네요.&lt;/p&gt;
&lt;pre id=&quot;code_1732941371493&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func stringToNumber(_ string:String) -&amp;gt; Int {
  return Int(string)!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 해설도 필요없지만, 변환하는 방법들을 적어보기 위해서 갖고왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String에서 Int로 변환할 때는 해당 String(string으로 명명) 값이 정확히 정수로 들어온다는 조건 하에 다음과 같이 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732941610842&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let returnValue = Int(string) // optional Int value&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Int의 생성자 중 String을 바로 받는게 있어서 가능한데요, 문서에서 관련 내용을 찾아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swift/int#Converting-Strings&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/swift/int#Converting-Strings&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1692&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcK6xp/btsK1IHKOSd/T7JPJjF3tUX0R1LlhE844K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcK6xp/btsK1IHKOSd/T7JPJjF3tUX0R1LlhE844K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcK6xp/btsK1IHKOSd/T7JPJjF3tUX0R1LlhE844K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcK6xp%2FbtsK1IHKOSd%2FT7JPJjF3tUX0R1LlhE844K%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;1692&quot; height=&quot;622&quot; data-origin-width=&quot;1692&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용에 따르면 string으로 받은 값을 optional Int로 반환해주는 생성자이네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 이런 Convert관련된 함수는 잘 되어있는 편입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 optional return value들은 optional이 아닌 변수에 할당할 수 없습니다. 별다른 조치를 취하지 않으면 다음과 같은 에러가 발생하겠죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Suve/btsK2tQtLP4/hKLmi4C3TTYKoYtgsHkpB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Suve/btsK2tQtLP4/hKLmi4C3TTYKoYtgsHkpB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Suve/btsK2tQtLP4/hKLmi4C3TTYKoYtgsHkpB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Suve%2FbtsK2tQtLP4%2FhKLmi4C3TTYKoYtgsHkpB0%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;1096&quot; height=&quot;168&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 기본값을 ?? 통해서 지정하거나&lt;/p&gt;
&lt;pre id=&quot;code_1732941973283&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let returnValue = Int(string) ?? 0 // not optional Int value&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. !를 통해서 force-unwrap을 해주거나.&lt;/p&gt;
&lt;pre id=&quot;code_1732941983965&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let returnValue = Int(string)! // optional Int value&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;당연히 2번 값의 경우는 강제종료가 일어날 수 있으니 주의해서 사용하셔야 합니다.&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;pre id=&quot;code_1732942060567&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let returnIntValue = (string as NSString).integerValue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 Objective-C 또는 초기 Swift를 사용하던 분들이라면 익숙할 NSString에 선언 되어있는 integerValue를 통해서 Int 값을 불러오는 것입니다. 이경우는 요즘은 사용하는 경우가 없을 것 같지만, 만에 하나 NSString을 다루는 경우에는 이런 방법도 있다 정도로 알고 계시면 될 것 같네요.&lt;/p&gt;</description>
      <category>Codewars(알고리즘)/8Kyu</category>
      <category>8kyu</category>
      <category>codewars</category>
      <category>Solution</category>
      <category>SWIFT</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/264</guid>
      <comments>https://dragoner.tistory.com/264#entry264comment</comments>
      <pubDate>Sat, 30 Nov 2024 15:00:45 +0900</pubDate>
    </item>
    <item>
      <title>[Codewars] [7Kyu] Highest and Lowest</title>
      <link>https://dragoner.tistory.com/265</link>
      <description>&lt;figure id=&quot;og_1732942385364&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Codewars - Achieve mastery through coding practice and developer mentorship&quot; data-og-description=&quot;A coding practice website for all programming levels &amp;ndash; Join a community of over 3 million developers and improve your coding skills in over 55 programming languages!&quot; data-og-host=&quot;www.codewars.com&quot; data-og-source-url=&quot;https://www.codewars.com/kata/554b4ac871d6813a03000035/train/swift&quot; data-og-url=&quot;https://www.codewars.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bUxpRl/hyXGMWnYvB/nq6GtPnOwfabwslwPjFCq0/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670,https://scrap.kakaocdn.net/dn/bV2ipI/hyXGABFGuw/poc7Ii6OMJ2qRul3Pyw1xK/img.png?width=1200&amp;amp;height=630&amp;amp;face=271_208_422_374&quot;&gt;&lt;a href=&quot;https://www.codewars.com/kata/554b4ac871d6813a03000035/train/swift&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.codewars.com/kata/554b4ac871d6813a03000035/train/swift&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bUxpRl/hyXGMWnYvB/nq6GtPnOwfabwslwPjFCq0/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670,https://scrap.kakaocdn.net/dn/bV2ipI/hyXGABFGuw/poc7Ii6OMJ2qRul3Pyw1xK/img.png?width=1200&amp;amp;height=630&amp;amp;face=271_208_422_374');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Codewars - Achieve mastery through coding practice and developer mentorship&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A coding practice website for all programming levels &amp;ndash; Join a community of over 3 million developers and improve your coding skills in over 55 programming languages!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.codewars.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 string으로 들어온 값 중 가장 큰 값과 작은 값을 찾아서 string으로 반환하는 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 해보자면 string을 특정 값을 기준으로 배열로 나누고, 각 배열의 아이템들을 Int로 변환 후 해당 값들의 max, min값을 찾으면 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이걸 다 풀어서 적어보자면 다음과 같이 할 수 있겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732943022369&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func highAndLow(_ numbers: String) -&amp;gt; String {
    var highest: Int?
    var lowest: Int?
    for element in numbers.components(separatedBy: &quot; &quot;) {
        guard let element = Int(element) else { continue }
        if let _highest = highest {
            if _highest &amp;lt; element {
                highest = element
            }
        } else {
            highest = element
        }
        if let _lowest = lowest {
            if _lowest &amp;gt; element {
                lowest = element
            }
        } else {
            lowest = element
        }
    }
    return &quot;\(highest ?? 0) \(lowest ?? 0)&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 동작을 따로 구현하지 않고 있는 것들로 사용하면 다음처럼 구현할 수 있겠네요.&lt;/p&gt;
&lt;pre id=&quot;code_1732943649810&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let numArray = numbers.split(separator: &quot; &quot;).compactMap{ Int($0) }
return &quot;\(numArray.max()!) \(numArray.min()!)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 가공이 필요 없다면 가독성도 좋고 코드 사용성도 좋으니 두번째 방식으로 쓰는게 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compactMap을 쓰는 이유는 Optional Binding과 nil 제거를 위해서 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Map을 사용한다면 반환 값은 [Int?]가 될테지만, CompactMap을 사용하면 [Int]가 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flatMap또한 compactMap과 같이 optional binding 및 nil 제거가 가능하지만, Swift 4.1부터는 1차원 배열에서 nil을 제거하고 옵셔널 바인딩을 하고자 할 때 compactMap을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 XCode에서도 경고를 반환하니까 참고하세요.(쓸 수는 있습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs5bJp/btsK12TrAwd/bYJnHOTbFaufk2ZkCefMGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs5bJp/btsK12TrAwd/bYJnHOTbFaufk2ZkCefMGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs5bJp/btsK12TrAwd/bYJnHOTbFaufk2ZkCefMGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs5bJp%2FbtsK12TrAwd%2FbYJnHOTbFaufk2ZkCefMGk%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;1086&quot; height=&quot;130&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Codewars(알고리즘)/7Kyu</category>
      <category>7kyu</category>
      <category>codewars</category>
      <category>Solution</category>
      <category>SWIFT</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/265</guid>
      <comments>https://dragoner.tistory.com/265#entry265comment</comments>
      <pubDate>Sat, 30 Nov 2024 15:00:18 +0900</pubDate>
    </item>
    <item>
      <title>[MacOS] 맥 시스템 정보 메뉴바 오픈소스, Mac Circle</title>
      <link>https://dragoner.tistory.com/259</link>
      <description>&lt;figure id=&quot;og_1695018968417&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - brk-ozs11/Mac-Circle: macOS power monitoring app is menu bar extra developed in SwiftUI&quot; data-og-description=&quot;macOS power monitoring app is menu bar extra developed in SwiftUI - GitHub - brk-ozs11/Mac-Circle: macOS power monitoring app is menu bar extra developed in SwiftUI&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/brk-ozs11/Mac-Circle&quot; data-og-url=&quot;https://github.com/brk-ozs11/Mac-Circle&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/71wj7/hyTY40toe0/8KX6hLinVQMm0IDZ46o5k0/img.png?width=1200&amp;amp;height=600&amp;amp;face=967_153_1066_261&quot;&gt;&lt;a href=&quot;https://github.com/brk-ozs11/Mac-Circle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/brk-ozs11/Mac-Circle&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/71wj7/hyTY40toe0/8KX6hLinVQMm0IDZ46o5k0/img.png?width=1200&amp;amp;height=600&amp;amp;face=967_153_1066_261');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - brk-ozs11/Mac-Circle: macOS power monitoring app is menu bar extra developed in SwiftUI&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;macOS power monitoring app is menu bar extra developed in SwiftUI - GitHub - brk-ozs11/Mac-Circle: macOS power monitoring app is menu bar extra developed in SwiftUI&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재밌어보이는 프로젝트라 재빨리 갖고와서 저장...ㅎㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI 공부할 겸 MacOS 앱 개발 공부와 함께 사용된 기술 분석하고 공부하면 재밌어 보였음.(코드도 길지 않아서 더욱 이해하기 좋아보이는 프로젝트)&lt;/p&gt;</description>
      <category>MacOS 프로그래밍/OpenSource-MacOS</category>
      <category>macos</category>
      <category>OpenSource</category>
      <category>study</category>
      <category>swiftUI</category>
      <category>System Info Menu bar</category>
      <author>Dannian</author>
      <guid isPermaLink="true">https://dragoner.tistory.com/259</guid>
      <comments>https://dragoner.tistory.com/259#entry259comment</comments>
      <pubDate>Mon, 18 Sep 2023 15:38:54 +0900</pubDate>
    </item>
  </channel>
</rss>