[GOP 프로젝트] 왜 사람들은 공부는 하는데도 성과가 없을까?

2025. 9. 8. 23:32생각을 말하다

왜 사람들은 공부는 하는데도 성과가 없을까?

 

 

여러분 혹시 군대에 다녀온 분들이라면 GOP라는 단어를 잘 아실 겁니다. 일반적으로 GOP(General Outpost)는 군사 용어로, 휴전선 최전방에 있는 초소를 말합니다. 적과 가장 가까이 맞닿아 있고, 그야말로 긴장의 끈을 단 한 순간도 놓칠 수 없는 곳이죠. 그런데 저는 오늘 여러분 인생에 이 GOP라는 개념을 한번 가져와 보려 합니다. 인생에도 최전방이 있습니다. 매일 우리가 부딪히는 공부, 일, 관계, 꿈을 향한 도전이 바로 인생의 GOP이죠.

그런데 아이러니하게도 수많은 사람들이 이 인생의 최전방에서 열심히 싸우는 것 같지만, 정작 성과는 미미합니다. 왜 그럴까요? 왜 어떤 사람은 몇 년, 아니 몇십 년 동안 공부하고 자격증 따고, 새로운 지식을 배우는데도 인생이 그다지 달라지지 않는 걸까요? 수많은 사람들이 새벽부터 밤까지 학원 다니고, 유튜브 강의 보고, 독서하고, 영어 공부에 컴퓨터 자격증까지 하면서도 왜 원하는 목표를 못 이루는 걸까요?

제가 보기에 가장 큰 문제는 사람들이 입력(Input) 에만 매달린다는 겁니다. 공부라고 하면 책상에 앉아서 책 읽고 필기하는 것, 강의 듣고 요약하는 것, 단어 외우는 걸로 생각합니다. 즉, 공부를 '지식의 입력'으로만 여기는 거죠. 그런데 인생이란 게 단순한 정보 입력만으로 바뀌지 않습니다. 마치 군대에서 매뉴얼만 백 번 읽는다고 전투를 잘할 수 있는 게 아닌 것과 똑같습니다. 전투는 실전 경험이 중요합니다. 전략을 세우고, 직접 움직이고, 실패에서 배우고, 다음 작전을 준비해야 하죠.

그런데 대부분의 사람들은 책상 앞에서 정보만 입력합니다. 열심히 적고, 외우고, 요약하지만 정작 출력(Output) 이 없습니다. 배운 걸 말해보지도 않고, 글로 써보지도 않고, 누구에게 가르쳐보지도 않습니다. 그냥 입력만 하고 끝내죠. 그러니 실력이 쌓이지 않고, 몇 년이 지나도 목표 달성이 안 되는 겁니다.

또 한 가지 문제는 핵심에 집중하지 못한다는 점입니다. 사람들은 책 한 권을 공부하면 처음부터 끝까지 다 알아야 한다고 생각합니다. 그러다 보니 정말 중요한 20%를 놓치고, 덜 중요한 80%에 시간과 에너지를 쏟아붓습니다. 마치 전쟁터에서 정작 중요한 고지를 버려두고 쓸데없는 언덕만 지키는 것과 같습니다.

마지막으로, 피드백이 없습니다. 배운 것을 돌아보고 잘한 것, 못한 것, 개선할 것을 정리해야 하는데 그런 과정 없이 그냥 또 다음 책, 다음 강의로 넘어갑니다. 그러니 같은 실수를 반복하고, 방향이 틀려도 모른 채 시간만 허비합니다.

이런 문제들을 해결하기 위해 저는 오늘 GOP 프로젝트를 제안합니다. 인생의 최전방에서 매일 실천할 수 있는 공부·성찰·성장의 세 가지 축을 연결해서, 단순히 공부만 하는 인생이 아니라 배우고, 돌아보고, 성장하는 인생을 만드는 방법입니다. 이 프로젝트의 세 가지 핵심은 바로 성장(GHC), 학습(OFT), 성찰(PMN) 입니다. 이제 이 세 가지를 하나씩 살펴보면서 구체적으로 설명드리겠습니다.

 

 

1: 성장 – GHC 모델

성장은 단순히 지식을 많이 쌓는 게 아닙니다. 우리가 추구해야 할 것은 인생 전체의 방향장기적인 변화입니다. 저는 이 성장을 세 가지 축으로 정리했습니다. 바로 Goal(목표), Habit(습관), Challenge(도전) 입니다. 그래서 GHC 모델이라고 부릅니다.

먼저 목표(Goal) 입니다. 목표는 인생에서 방향을 정해주는 나침반과 같습니다. 아무리 열심히 공부해도 목표가 없으면 결국 제자리걸음이 됩니다. 많은 사람들이 이 부분을 놓칩니다. 그냥 영어 공부하면 좋다니까 하고, 코딩 배우면 좋다니까 배우고, 자격증 따면 유리하다니까 따는 식입니다. 그런데 목표 없이 배우는 지식은 흩어진 모래알과 같아서 힘을 발휘하지 못합니다. 그래서 GHC의 첫 번째는 반드시 구체적이고 측정 가능하며 달성 가능한 목표를 설정하는 것입니다. 예를 들어 “3개월 안에 영어 단어 1000개를 외운다”처럼 말이죠.

두 번째는 습관(Habit) 입니다. 장기 목표를 이루는 힘은 거창한 결심이 아니라 매일 반복되는 습관에서 나옵니다. 하루 10분이라도 꾸준히 영어 단어를 외우는 사람과 한 달에 한 번 벼락치기 하는 사람 중 누가 더 실력이 늘까요? 당연히 전자입니다. 습관이 무서운 이유는 한 번 굳어지면 자동화된다는 데 있습니다. 이를 위해서는 작은 목표부터 시작해 점점 키워가는 전략이 필요합니다.

세 번째는 도전(Challenge) 입니다. 배운 것을 실제로 써보는 경험이 없으면 성장하지 못합니다. 시험에 도전하고, 발표를 해보고, 누군가에게 가르쳐보고, 때로는 실패도 해봐야 합니다. 전투 경험 없는 군인이 매뉴얼만으로 전쟁을 이길 수 없는 것처럼, 경험 없는 지식은 쉽게 사라집니다.

목표 단계(GHC):
Goal(목표) – 방향과 우선순위를 설정한다.(예: 3개월 안에 영어 단어 1000개 외우기)
Habit(습관) – 작은 행동을 매일 반복해 자동화한다.(예: 매일 아침 10분 독서, 3줄 실천법 기록)
Challenge(도전) – 배운 것을 실제 경험과 프로젝트로 연결한다.(예: 영어 글쓰기 대회 참가, 봉사활동 발표)

GHC 모델은 이렇게 목표 → 습관 → 도전으로 이어지면서 장기적인 성장을 설계하게 해줍니다. 그러나 성장만 있다고 인생이 바뀌지는 않습니다. 매일 무엇을 배우고 어떻게 실천할지에 대한 학습 전략이 필요합니다. 그것이 바로 두 번째 모델, OFT입니다.

 

2: 학습 – OFT 모델

OFT는 Output(출력), Focus(집중), Teach(가르치기) 의 약자입니다. 대부분의 사람들은 공부하면 책 읽고 강의 듣는 입력(Input)만 떠올립니다. 그러나 실제로 기억에 오래 남고 실력이 되는 건 출력(Output) 입니다. 배운 걸 글로 정리하고, 말로 설명하고, 문제를 풀어봐야 비로소 자기 것이 됩니다. 그래서 공부할 때는 반드시 출력 중심으로 접근해야 합니다.

두 번째는 집중(Focus) 입니다. 파레토 법칙 아시죠? 결과의 80%는 원인의 20%에서 나온다는 법칙입니다. 공부도 마찬가지입니다. 한 권의 책에서 정말 중요한 핵심은 일부에 불과합니다. 그런데 사람들은 처음부터 끝까지 다 외우려고 합니다. 그럴 필요 없습니다. 핵심을 파악하고 그 부분에 집중하는 게 효율적인 공부의 비밀입니다.

세 번째는 가르치기(Teach) 입니다. 내가 아는 걸 누군가에게 설명할 수 없으면 제대로 이해한 게 아닙니다. 그래서 친구에게 설명하거나 블로그에 글을 쓰거나 유튜브 영상을 찍어보는 게 공부에 엄청난 도움이 됩니다. 가르치는 과정에서 내가 모르는 부분이 드러나고, 지식이 체계화되기 때문입니다.

OFT 모델은 이렇게 출력 → 핵심 집중 → 가르치기로 이어지면서 매일의 학습을 단순한 정보 입력이 아니라 실력으로 전환시키는 과정으로 바꿔줍니다.

학습 단계(OFT):
① Output(출력) → 배우고 정리하고 말해보고
② Focus(집중) → 핵심만 집중하고
③ Teach(설명) → 남에게 설명하며 완전한 이해

 

 3: 성찰 – PMN 모델

마지막으로 소개할 모델은 PMN입니다. 즉, Plus(잘한 것), Minus(못한 것), Next(개선할 것) 입니다. 우리가 공부하고 실천한 뒤에 반드시 해야 하는 게 바로 성찰입니다. 잘한 것은 무엇인지, 부족한 것은 무엇인지, 그리고 내일은 무엇을 바꿀 것인지 정리하지 않으면 같은 실수를 반복하게 됩니다.

예를 들어 오늘 영어 단어 50개를 외우기로 했는데 30개밖에 못 외웠다면, 잘한 점은 30개나마 외운 것이고, 못한 점은 계획을 끝까지 지키지 못한 것이며, 개선할 점은 내일은 25개씩 두 번에 나눠서 외우는 식으로 바꾸는 겁니다. 이렇게 하면 매일 조금씩 더 나아지게 됩니다.

PMN은 하루 5분이면 충분합니다. 공부뿐 아니라 직장 회의, 프로젝트, 인간관계, 심지어 가정생활까지도 이 3단계 성찰법으로 돌아보면 삶이 훨씬 체계적으로 변합니다.

성찰·피드백 단계(PMN):
① Plus(잘한것) → 오늘 잘한 점 기록
② Minus(못한것) → 부족한 점 확인
③ Next(개선할것) → 내일의 실천 계획 세움

 

GOP 프로젝트가 인생에 미치는 영향

이제 정리해보겠습니다. 우리가 제안한 GOP 프로젝트는 인생의 최전방에서 매일 배우고, 돌아보고, 성장하는 모델입니다. GHC로 장기적인 목표와 방향을 세우고, OFT로 매일의 학습을 실행하며, PMN으로 피드백과 개선을 이어가는 구조죠.

성장(Goal·Habit·Challenge) → 장기 목표와 방향 설정
학습(Output·Focus·Teach) → 매일 실천하며 지식과 기술 습득
성찰(Plus·Minus·Next) → 돌아보고 개선 계획 세우기

이 프로세스가 인생에 미치는 영향은 엄청납니다. 단순히 책상 앞에서 공부만 하는 사람이 아니라, 배운 걸 실천하고 성찰하면서 인생을 조금씩 바꿔가는 사람이 됩니다. 예를 들어, 영어 공부를 1년 동안 했는데도 말 한마디 못하는 사람이 있는 반면, 어떤 사람은 6개월 만에 프레젠테이션까지 하는 경우가 있습니다. 차이는 방법에 있습니다. 배우고(Output), 핵심에 집중하고(Focus), 남에게 가르치고(Teach), 잘한 것과 못한 것을 돌아보고(Plus, Minus), 개선 계획을 세우고(Next), 장기 목표와 습관, 도전으로 연결시키는 사람은 성장할 수밖에 없습니다.

군대의 GOP가 나라의 최전방을 지키듯이, 인생의 GOP 프로젝트는 여러분의 인생 최전방을 바꿔놓을 것입니다. 매일 이 과정을 반복하면 1년 후, 3년 후, 5년 후 여러분의 인생은 전혀 다른 모습이 될 겁니다. 작은 습관이 쌓이고, 경험이 쌓이고, 피드백이 반복되면서 결국 여러분은 원하는 목표를 이룰 수 있게 됩니다.

그러니 이제는 공부만 하지 말고, 배우고, 성찰하고, 성장하는 인생을 시작해보세요. 그것이 바로 인생의 최전방에서 승리하는 길입니다.

 

아래는 위의 내용을 앱으로 구현하기 위한 프로토타입입니다.

 

 

 

import React, { useEffect, useState } from "react";

// GOP 프로젝트 - 홈 화면 프로토타입 (예시 동작 포함)
// - Top3 편집, OFT 입력, PMN 3줄 성찰, GHC 보드/월간 도전 설정 예시 구현
// - 외부 라이브러리 없이 상태만으로 동작하는 간단 프로토타입

// 간단 모달 컴포넌트
function Modal({ open, title, onClose, children }: { open: boolean; title: string; onClose: () => void; children: React.ReactNode }) {
  if (!open) return null;
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
      <div className="w-full max-w-lg rounded-2xl bg-white p-4 shadow-xl">
        <div className="mb-3 flex items-center justify-between">
          <h3 className="text-lg font-semibold">{title}</h3>
          <button onClick={onClose} className="rounded-md px-2 py-1 text-sm text-gray-600 hover:bg-gray-100">닫기</button>
        </div>
        {children}
      </div>
    </div>
  );
}

export default function HomeScreen() {
  // 상태: Top3, OFT, PMN, GHC
  const [top3, setTop3] = useState<string[]>(["영어 단어 20개 외우기", "운동 30분", "독서 20분"]);
  const [oftLog, setOftLog] = useState<{ date: string; output: string; focus: string; teach: string }[]>([]);
  const [pmnLog, setPmnLog] = useState<{ date: string; plus: string; minus: string; next: string }[]>([]);
  const [goal, setGoal] = useState<string>("3개월 내 영어 단어 1000개");
  const [habit, setHabit] = useState<string>("매일 20분 단어 학습");
  const [challenge, setChallenge] = useState<string>("이번 달 20일 연속 실천");

  // 모달 상태
  const [openEditTop3, setOpenEditTop3] = useState(false);
  const [openOFT, setOpenOFT] = useState(false);
  const [openPMN, setOpenPMN] = useState(false);
  const [openTimeline, setOpenTimeline] = useState(false);
  const [openBoard, setOpenBoard] = useState(false);
  const [openChallenge, setOpenChallenge] = useState(false);

  // 스모크 테스트 (웹 환경에서만)
  useEffect(() => {
    try {
      if (typeof document === "undefined") return;
      const ok = ["#gop-title", "#top3", "#section-oft", "#section-pmn", "#section-ghc"].every((sel) => !!document.querySelector(sel));
      if (ok) console.log("[TEST] 홈 화면 스모크 테스트 통과 ✅");
    } catch {}
  }, []);

  // 유틸
  const today = new Date().toISOString().slice(0, 10);

  // 예시 동작 핸들러
  const handleSaveTop3 = (vals: string[]) => {
    setTop3(vals.map((v) => v.trim()).filter(Boolean).slice(0, 3));
    setOpenEditTop3(false);
  };

  const handleSaveOFT = (output: string, focus: string, teach: string) => {
    setOftLog((prev) => [{ date: today, output, focus, teach }, ...prev]);
    setOpenOFT(false);
  };

  const handleSavePMN = (plus: string, minus: string, next: string) => {
    setPmnLog((prev) => [{ date: today, plus, minus, next }, ...prev]);
    setOpenPMN(false);
  };

  const doneDaysThisMonth = pmnLog.filter((l) => l.date.startsWith(today.slice(0, 7))).length;

  return (
    <div className="min-h-screen w-full bg-white text-gray-900">
      {/* Header */}
      <header className="px-4 pt-6 pb-4 text-center">
        <h1 id="gop-title" className="text-3xl font-extrabold tracking-tight">GOP 프로젝트</h1>
        <p className="mt-1 text-sm text-gray-500">배우고 · 성찰하고 · 성장하는 하루</p>
      </header>

      <main className="mx-auto w-full max-w-2xl px-4 pb-10 space-y-6">
        {/* Top 3 Focus Section */}
        <section id="top3" className="rounded-2xl bg-blue-50 p-4 shadow-sm">
          <div className="flex items-center justify-between">
            <h2 className="text-lg font-semibold">오늘의 Top 3 집중</h2>
            <button onClick={() => setOpenEditTop3(true)} className="rounded-xl px-3 py-1 text-sm font-medium text-blue-700 hover:bg-blue-100">편집</button>
          </div>
          <ul className="mt-2 list-decimal pl-5 space-y-1">
            {top3.map((t, i) => (
              <li key={i}>{t}</li>
            ))}
          </ul>
        </section>

        {/* OFT Section */}
        <section id="section-oft" className="rounded-2xl bg-green-50 p-4 shadow-sm">
          <h2 className="text-lg font-semibold mb-1">OFT - 배우기</h2>
          <p className="text-sm text-gray-700 mb-3">Output · Focus · Teach 순서로 학습 기록</p>
          <div className="flex gap-2">
            <button onClick={() => setOpenOFT(true)} className="rounded-xl bg-green-600 px-4 py-2 text-white font-medium hover:bg-green-700">학습 시작하기</button>
            <button onClick={() => setOpenTimeline(true)} className="rounded-xl border border-green-600 px-4 py-2 text-green-700 font-medium hover:bg-green-100">출력 기록 보기</button>
          </div>
        </section>

        {/* PMN Section */}
        <section id="section-pmn" className="rounded-2xl bg-yellow-50 p-4 shadow-sm">
          <h2 className="text-lg font-semibold mb-1">PMN - 성찰하기</h2>
          <p className="text-sm text-gray-700 mb-3">+ 잘한 점 / − 아쉬운 점 / ⇒ 내일 할 것</p>
          <div className="flex gap-2">
            <button onClick={() => setOpenPMN(true)} className="rounded-xl bg-yellow-500 px-4 py-2 text-white font-medium hover:bg-yellow-600">3줄 성찰 작성</button>
            <button onClick={() => setOpenTimeline(true)} className="rounded-xl border border-yellow-500 px-4 py-2 text-yellow-700 font-medium hover:bg-yellow-100">성찰 타임라인</button>
          </div>
        </section>

        {/* GHC Growth Board */}
        <section id="section-ghc" className="rounded-2xl bg-purple-50 p-4 shadow-sm">
          <h2 className="text-lg font-semibold mb-1">GHC 성장 보드</h2>
          <p className="text-sm text-gray-700 mb-3">Goal · Habit · Challenge 진행률 확인</p>
          <div className="flex flex-col gap-2">
            <div className="rounded-xl bg-white p-3 text-sm">
              <div><span className="font-semibold">Goal:</span> {goal}</div>
              <div><span className="font-semibold">Habit:</span> {habit}</div>
              <div><span className="font-semibold">Challenge:</span> {challenge} <span className="text-gray-500">(이번 달 성찰 기록 {doneDaysThisMonth}일)</span></div>
            </div>
            <div className="flex gap-2">
              <button onClick={() => setOpenBoard(true)} className="rounded-xl bg-purple-600 px-4 py-2 text-white font-medium hover:bg-purple-700">보드 보기</button>
              <button onClick={() => setOpenChallenge(true)} className="rounded-xl border border-purple-600 px-4 py-2 text-purple-700 font-medium hover:bg-purple-100">월간 도전 설정</button>
            </div>
          </div>
        </section>
      </main>

      {/* Footer */}
      <footer className="pb-8 text-center text-xs text-gray-400">© GOP Project · Beta</footer>

      {/* 모달들 */}
      <Modal open={openEditTop3} title="오늘의 Top 3 편집" onClose={() => setOpenEditTop3(false)}>
        <Top3Editor initial={top3} onSave={handleSaveTop3} />
      </Modal>

      <Modal open={openOFT} title="OFT 학습 기록" onClose={() => setOpenOFT(false)}>
        <OFTForm onSave={handleSaveOFT} />
      </Modal>

      <Modal open={openPMN} title="PMN 3줄 성찰" onClose={() => setOpenPMN(false)}>
        <PMNForm onSave={handleSavePMN} />
      </Modal>

      <Modal open={openTimeline} title="기록 타임라인" onClose={() => setOpenTimeline(false)}>
        <Timeline oft={oftLog} pmn={pmnLog} />
      </Modal>

      <Modal open={openBoard} title="GHC 보드" onClose={() => setOpenBoard(false)}>
        <GHCBoard goal={goal} habit={habit} challenge={challenge} pmnCount={doneDaysThisMonth} />
      </Modal>

      <Modal open={openChallenge} title="월간 도전 설정" onClose={() => setOpenChallenge(false)}>
        <ChallengeForm current={{ goal, habit, challenge }} onSave={(g, h, c) => { setGoal(g); setHabit(h); setChallenge(c); setOpenChallenge(false); }} />
      </Modal>
    </div>
  );
}

// === 하위 폼/뷰 컴포넌트 ===
function Top3Editor({ initial, onSave }: { initial: string[]; onSave: (vals: string[]) => void }) {
  const [v1, setV1] = useState(initial[0] ?? "");
  const [v2, setV2] = useState(initial[1] ?? "");
  const [v3, setV3] = useState(initial[2] ?? "");
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        onSave([v1, v2, v3]);
      }}
      className="space-y-2"
    >
      <input className="w-full rounded-lg border p-2" value={v1} onChange={(e) => setV1(e.target.value)} placeholder="1번째 집중" />
      <input className="w-full rounded-lg border p-2" value={v2} onChange={(e) => setV2(e.target.value)} placeholder="2번째 집중" />
      <input className="w-full rounded-lg border p-2" value={v3} onChange={(e) => setV3(e.target.value)} placeholder="3번째 집중" />
      <div className="pt-2 text-right">
        <button className="rounded-xl bg-blue-600 px-4 py-2 text-white hover:bg-blue-700" type="submit">저장</button>
      </div>
    </form>
  );
}

function OFTForm({ onSave }: { onSave: (output: string, focus: string, teach: string) => void }) {
  const [output, setOutput] = useState("");
  const [focus, setFocus] = useState("");
  const [teach, setTeach] = useState("");
  return (
    <form
      onSubmit={(e) => { e.preventDefault(); onSave(output, focus, teach); }}
      className="space-y-2"
    >
      <textarea className="w-full rounded-lg border p-2" rows={3} value={output} onChange={(e) => setOutput(e.target.value)} placeholder="Output: 오늘 배운 것을 요약" />
      <input className="w-full rounded-lg border p-2" value={focus} onChange={(e) => setFocus(e.target.value)} placeholder="Focus: 핵심 20%" />
      <textarea className="w-full rounded-lg border p-2" rows={2} value={teach} onChange={(e) => setTeach(e.target.value)} placeholder="Teach: 누군가에게 설명하듯 정리" />
      <div className="pt-2 text-right">
        <button className="rounded-xl bg-green-600 px-4 py-2 text-white hover:bg-green-700" type="submit">저장</button>
      </div>
    </form>
  );
}

function PMNForm({ onSave }: { onSave: (plus: string, minus: string, next: string) => void }) {
  const [plus, setPlus] = useState("");
  const [minus, setMinus] = useState("");
  const [next, setNext] = useState("");
  return (
    <form onSubmit={(e) => { e.preventDefault(); onSave(plus, minus, next); }} className="space-y-2">
      <input className="w-full rounded-lg border p-2" value={plus} onChange={(e) => setPlus(e.target.value)} placeholder="+ 잘한 점" />
      <input className="w-full rounded-lg border p-2" value={minus} onChange={(e) => setMinus(e.target.value)} placeholder="− 아쉬운 점" />
      <input className="w-full rounded-lg border p-2" value={next} onChange={(e) => setNext(e.target.value)} placeholder="⇒ 내일 할 것" />
      <div className="pt-2 text-right">
        <button className="rounded-xl bg-yellow-500 px-4 py-2 text-white hover:bg-yellow-600" type="submit">저장</button>
      </div>
    </form>
  );
}

function Timeline({ oft, pmn }: { oft: { date: string; output: string; focus: string; teach: string }[]; pmn: { date: string; plus: string; minus: string; next: string }[] }) {
  return (
    <div className="max-h-[60vh] overflow-y-auto space-y-4">
      <div>
        <h4 className="mb-2 font-semibold">OFT 기록</h4>
        <ul className="space-y-2 text-sm">
          {oft.length === 0 && <li className="text-gray-500">기록 없음</li>}
          {oft.map((o, i) => (
            <li key={`oft-${i}`} className="rounded-lg border p-2">
              <div className="text-xs text-gray-500">{o.date}</div>
              <div><span className="font-semibold">Output:</span> {o.output}</div>
              <div><span className="font-semibold">Focus:</span> {o.focus}</div>
              <div><span className="font-semibold">Teach:</span> {o.teach}</div>
            </li>
          ))}
        </ul>
      </div>
      <div>
        <h4 className="mb-2 font-semibold">PMN 성찰</h4>
        <ul className="space-y-2 text-sm">
          {pmn.length === 0 && <li className="text-gray-500">기록 없음</li>}
          {pmn.map((p, i) => (
            <li key={`pmn-${i}`} className="rounded-lg border p-2">
              <div className="text-xs text-gray-500">{p.date}</div>
              <div><span className="font-semibold">+</span> {p.plus}</div>
              <div><span className="font-semibold">−</span> {p.minus}</div>
              <div><span className="font-semibold">⇒</span> {p.next}</div>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

function GHCBoard({ goal, habit, challenge, pmnCount }: { goal: string; habit: string; challenge: string; pmnCount: number }) {
  return (
    <div className="space-y-3 text-sm">
      <div className="rounded-xl border p-3">
        <div><span className="font-semibold">Goal:</span> {goal}</div>
        <div><span className="font-semibold">Habit:</span> {habit}</div>
        <div><span className="font-semibold">Challenge:</span> {challenge}</div>
      </div>
      <div className="rounded-xl border p-3">
        <div className="mb-1 font-semibold">이번 달 성찰 진행률(일 수)</div>
        <div>{pmnCount} / 30 (예시)</div>
      </div>
    </div>
  );
}

function ChallengeForm({ current, onSave }: { current: { goal: string; habit: string; challenge: string }; onSave: (g: string, h: string, c: string) => void }) {
  const [g, setG] = useState(current.goal);
  const [h, setH] = useState(current.habit);
  const [c, setC] = useState(current.challenge);
  return (
    <form onSubmit={(e) => { e.preventDefault(); onSave(g, h, c); }} className="space-y-2">
      <input className="w-full rounded-lg border p-2" value={g} onChange={(e) => setG(e.target.value)} placeholder="장기 목표(Goal)" />
      <input className="w-full rounded-lg border p-2" value={h} onChange={(e) => setH(e.target.value)} placeholder="매일 습관(Habit)" />
      <input className="w-full rounded-lg border p-2" value={c} onChange={(e) => setC(e.target.value)} placeholder="월간 도전(Challenge)" />
      <div className="pt-2 text-right">
        <button className="rounded-xl bg-purple-600 px-4 py-2 text-white hover:bg-purple-700" type="submit">저장</button>
      </div>
    </form>
  );
}

/*
====================================
🧪 TESTS (Jest + React Testing Library) – 예시 시나리오 추가
------------------------------------
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import HomeScreen from './HomeScreen';

describe('HomeScreen 예시 동작', () => {
  test('기본 섹션 및 버튼 렌더링', () => {
    render(<HomeScreen />);
    expect(screen.getByText('GOP 프로젝트')).toBeInTheDocument();
    expect(screen.getByText('오늘의 Top 3 집중')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '편집' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '학습 시작하기' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '3줄 성찰 작성' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '보드 보기' })).toBeInTheDocument();
  });

  test('Top3 편집 모달에서 저장', () => {
    render(<HomeScreen />);
    fireEvent.click(screen.getByRole('button', { name: '편집' }));
    const input1 = screen.getByPlaceholderText('1번째 집중');
    fireEvent.change(input1, { target: { value: '수학 10문제' } });
    fireEvent.click(screen.getByRole('button', { name: '저장' }));
    expect(screen.getByText('수학 10문제')).toBeInTheDocument();
  });

  test('OFT 저장 후 타임라인 표시', () => {
    render(<HomeScreen />);
    fireEvent.click(screen.getByRole('button', { name: '학습 시작하기' }));
    fireEvent.change(screen.getByPlaceholderText('Output: 오늘 배운 것을 요약'), { target: { value: '분수 덧셈 이해' } });
    fireEvent.change(screen.getByPlaceholderText('Focus: 핵심 20%'), { target: { value: '통분 규칙' } });
    fireEvent.change(screen.getByPlaceholderText('Teach: 누군가에게 설명하듯 정리'), { target: { value: '친구에게 설명' } });
    fireEvent.click(screen.getByRole('button', { name: '저장' }));
    fireEvent.click(screen.getByRole('button', { name: '출력 기록 보기' }));
    expect(screen.getByText('분수 덧셈 이해')).toBeInTheDocument();
    expect(screen.getByText('통분 규칙')).toBeInTheDocument();
  });

  test('PMN 3줄 성찰 저장', () => {
    render(<HomeScreen />);
    fireEvent.click(screen.getByRole('button', { name: '3줄 성찰 작성' }));
    fireEvent.change(screen.getByPlaceholderText('+ 잘한 점'), { target: { value: '시간 지켜 공부' } });
    fireEvent.change(screen.getByPlaceholderText('− 아쉬운 점'), { target: { value: '집중력 흔들림' } });
    fireEvent.change(screen.getByPlaceholderText('⇒ 내일 할 것'), { target: { value: '25분 타이머 2회' } });
    fireEvent.click(screen.getByRole('button', { name: '저장' }));
    fireEvent.click(screen.getByRole('button', { name: '성찰 타임라인' }));
    expect(screen.getByText('시간 지켜 공부')).toBeInTheDocument();
    expect(screen.getByText('25분 타이머 2회')).toBeInTheDocument();
  });
});
====================================
*/

 

gop_app_home.jsx
0.02MB

 

import React, { useEffect, useState } from "react";

// GOP 프로젝트 - 홈 화면 프로토타입 (예시 동작 포함)
// - Top3 편집, OFT 입력, PMN 3줄 성찰, GHC 보드/월간 도전 설정 예시 구현
// - 외부 라이브러리 없이 상태만으로 동작하는 간단 프로토타입

// 간단 모달 컴포넌트
function Modal({ open, title, onClose, children }: { open: boolean; title: string; onClose: () => void; children: React.ReactNode }) {
  if (!open) return null;
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
      <div className="w-full max-w-lg rounded-2xl bg-white p-4 shadow-xl">
        <div className="mb-3 flex items-center justify-between">
          <h3 className="text-lg font-semibold">{title}</h3>
          <button onClick={onClose} className="rounded-md px-2 py-1 text-sm text-gray-600 hover:bg-gray-100">닫기</button>
        </div>
        {children}
      </div>
    </div>
  );
}

export default function HomeScreen() {
  // 상태: Top3, OFT, PMN, GHC
  const [top3, setTop3] = useState<string[]>(["영어 단어 20개 외우기", "운동 30분", "독서 20분"]);
  const [oftLog, setOftLog] = useState<{ date: string; output: string; focus: string; teach: string }[]>([]);
  const [pmnLog, setPmnLog] = useState<{ date: string; plus: string; minus: string; next: string }[]>([]);
  const [goal, setGoal] = useState<string>("3개월 내 영어 단어 1000개");
  const [habit, setHabit] = useState<string>("매일 20분 단어 학습");
  const [challenge, setChallenge] = useState<string>("이번 달 20일 연속 실천");

  // 모달 상태
  const [openEditTop3, setOpenEditTop3] = useState(false);
  const [openOFT, setOpenOFT] = useState(false);
  const [openPMN, setOpenPMN] = useState(false);
  const [openTimeline, setOpenTimeline] = useState(false);
  const [openBoard, setOpenBoard] = useState(false);
  const [openChallenge, setOpenChallenge] = useState(false);

  // 스모크 테스트 (웹 환경에서만)
  useEffect(() => {
    try {
      if (typeof document === "undefined") return;
      const ok = ["#gop-title", "#top3", "#section-oft", "#section-pmn", "#section-ghc"].every((sel) => !!document.querySelector(sel));
      if (ok) console.log("[TEST] 홈 화면 스모크 테스트 통과 ✅");
    } catch {}
  }, []);

  // 유틸
  const today = new Date().toISOString().slice(0, 10);

  // 예시 동작 핸들러
  const handleSaveTop3 = (vals: string[]) => {
    setTop3(vals.map((v) => v.trim()).filter(Boolean).slice(0, 3));
    setOpenEditTop3(false);
  };

  const handleSaveOFT = (output: string, focus: string, teach: string) => {
    setOftLog((prev) => [{ date: today, output, focus, teach }, ...prev]);
    setOpenOFT(false);
  };

  const handleSavePMN = (plus: string, minus: string, next: string) => {
    setPmnLog((prev) => [{ date: today, plus, minus, next }, ...prev]);
    setOpenPMN(false);
  };

  const doneDaysThisMonth = pmnLog.filter((l) => l.date.startsWith(today.slice(0, 7))).length;

  return (
    <div className="min-h-screen w-full bg-white text-gray-900">
      {/* Header */}
      <header className="px-4 pt-6 pb-4 text-center">
        <h1 id="gop-title" className="text-3xl font-extrabold tracking-tight">GOP 프로젝트</h1>
        <p className="mt-1 text-sm text-gray-500">배우고 · 성찰하고 · 성장하는 하루</p>
      </header>

      <main className="mx-auto w-full max-w-2xl px-4 pb-10 space-y-6">
        {/* GOP 진행 표시 */}
        <div className="rounded-2xl bg-gray-50 p-3 text-center text-sm text-gray-700">
          <span className="font-semibold">G</span>rowth → <span className="font-semibold">O</span>utput/Focus/Teach → <span className="font-semibold">P</span>lus/Minus/Next
        </div>

        {/* G: GHC Growth Board (먼저 노출) */}
        <section id="section-ghc" className="rounded-2xl bg-purple-50 p-4 shadow-sm">
          <h2 className="text-lg font-semibold mb-1">G — GHC 성장 보드</h2>
          <p className="text-sm text-gray-700 mb-3">Goal · Habit · Challenge 진행률 확인</p>
          <div className="flex flex-col gap-2">
            <div className="rounded-xl bg-white p-3 text-sm">
              <div><span className="font-semibold">Goal:</span> {goal}</div>
              <div><span className="font-semibold">Habit:</span> {habit}</div>
              <div><span className="font-semibold">Challenge:</span> {challenge} <span className="text-gray-500">(이번 달 성찰 기록 {doneDaysThisMonth}일)</span></div>
            </div>
            <div className="flex gap-2">
              <button onClick={() => setOpenBoard(true)} className="rounded-xl bg-purple-600 px-4 py-2 text-white font-medium hover:bg-purple-700">보드 보기</button>
              <button onClick={() => setOpenChallenge(true)} className="rounded-xl border border-purple-600 px-4 py-2 text-purple-700 font-medium hover:bg-purple-100">월간 도전 설정</button>
            </div>
          </div>
        </section>

        {/* O: Top 3 Focus Section (OFT의 사전선택) */}
        <section id="top3" className="rounded-2xl bg-blue-50 p-4 shadow-sm">
          <div className="flex items-center justify-between">
            <h2 className="text-lg font-semibold">O — 오늘의 Top 3 집중</h2>
            <button onClick={() => setOpenEditTop3(true)} className="rounded-xl px-3 py-1 text-sm font-medium text-blue-700 hover:bg-blue-100">편집</button>
          </div>
          <ul className="mt-2 list-decimal pl-5 space-y-1">
            {top3.map((t, i) => (
              <li key={i}>{t}</li>
            ))}
          </ul>
        </section>

        {/* O: OFT Section */}
        <section id="section-oft" className="rounded-2xl bg-green-50 p-4 shadow-sm">
          <h2 className="text-lg font-semibold mb-1">O — OFT 배우기</h2>
          <p className="text-sm text-gray-700 mb-3">Output · Focus · Teach 순서로 학습 기록</p>
          <div className="flex gap-2">
            <button onClick={() => setOpenOFT(true)} className="rounded-xl bg-green-600 px-4 py-2 text-white font-medium hover:bg-green-700">학습 시작하기</button>
            <button onClick={() => setOpenTimeline(true)} className="rounded-xl border border-green-600 px-4 py-2 text-green-700 font-medium hover:bg-green-100">출력 기록 보기</button>
          </div>
        </section>

        {/* P: PMN Section */}
        <section id="section-pmn" className="rounded-2xl bg-yellow-50 p-4 shadow-sm">
          <h2 className="text-lg font-semibold mb-1">P — PMN 성찰하기</h2>
          <p className="text-sm text-gray-700 mb-3">+ 잘한 점 / − 아쉬운 점 / ⇒ 내일 할 것</p>
          <div className="flex gap-2">
            <button onClick={() => setOpenPMN(true)} className="rounded-xl bg-yellow-500 px-4 py-2 text-white font-medium hover:bg-yellow-600">3줄 성찰 작성</button>
            <button onClick={() => setOpenTimeline(true)} className="rounded-xl border border-yellow-500 px-4 py-2 text-yellow-700 font-medium hover:bg-yellow-100">성찰 타임라인</button>
          </div>
        </section>
      </main>

      {/* Footer */}
      <footer className="pb-8 text-center text-xs text-gray-400">© GOP Project · Beta</footer>

      {/* 모달들 */}
      <Modal open={openEditTop3} title="오늘의 Top 3 편집" onClose={() => setOpenEditTop3(false)}>
        <Top3Editor initial={top3} onSave={handleSaveTop3} />
      </Modal>

      <Modal open={openOFT} title="OFT 학습 기록" onClose={() => setOpenOFT(false)}>
        <OFTForm onSave={handleSaveOFT} />
      </Modal>

      <Modal open={openPMN} title="PMN 3줄 성찰" onClose={() => setOpenPMN(false)}>
        <PMNForm onSave={handleSavePMN} />
      </Modal>

      <Modal open={openTimeline} title="기록 타임라인" onClose={() => setOpenTimeline(false)}>
        <Timeline oft={oftLog} pmn={pmnLog} />
      </Modal>

      <Modal open={openBoard} title="GHC 보드" onClose={() => setOpenBoard(false)}>
        <GHCBoard goal={goal} habit={habit} challenge={challenge} pmnCount={doneDaysThisMonth} />
      </Modal>

      <Modal open={openChallenge} title="월간 도전 설정" onClose={() => setOpenChallenge(false)}>
        <ChallengeForm current={{ goal, habit, challenge }} onSave={(g, h, c) => { setGoal(g); setHabit(h); setChallenge(c); setOpenChallenge(false); }} />
      </Modal>
    </div>
  );
}

// === 하위 폼/뷰 컴포넌트 ===
function Top3Editor({ initial, onSave }: { initial: string[]; onSave: (vals: string[]) => void }) {
  const [v1, setV1] = useState(initial[0] ?? "");
  const [v2, setV2] = useState(initial[1] ?? "");
  const [v3, setV3] = useState(initial[2] ?? "");
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        onSave([v1, v2, v3]);
      }}
      className="space-y-2"
    >
      <input className="w-full rounded-lg border p-2" value={v1} onChange={(e) => setV1(e.target.value)} placeholder="1번째 집중" />
      <input className="w-full rounded-lg border p-2" value={v2} onChange={(e) => setV2(e.target.value)} placeholder="2번째 집중" />
      <input className="w-full rounded-lg border p-2" value={v3} onChange={(e) => setV3(e.target.value)} placeholder="3번째 집중" />
      <div className="pt-2 text-right">
        <button className="rounded-xl bg-blue-600 px-4 py-2 text-white hover:bg-blue-700" type="submit">저장</button>
      </div>
    </form>
  );
}

function OFTForm({ onSave }: { onSave: (output: string, focus: string, teach: string) => void }) {
  const [output, setOutput] = useState("");
  const [focus, setFocus] = useState("");
  const [teach, setTeach] = useState("");
  return (
    <form
      onSubmit={(e) => { e.preventDefault(); onSave(output, focus, teach); }}
      className="space-y-2"
    >
      <textarea className="w-full rounded-lg border p-2" rows={3} value={output} onChange={(e) => setOutput(e.target.value)} placeholder="Output: 오늘 배운 것을 요약" />
      <input className="w-full rounded-lg border p-2" value={focus} onChange={(e) => setFocus(e.target.value)} placeholder="Focus: 핵심 20%" />
      <textarea className="w-full rounded-lg border p-2" rows={2} value={teach} onChange={(e) => setTeach(e.target.value)} placeholder="Teach: 누군가에게 설명하듯 정리" />
      <div className="pt-2 text-right">
        <button className="rounded-xl bg-green-600 px-4 py-2 text-white hover:bg-green-700" type="submit">저장</button>
      </div>
    </form>
  );
}

function PMNForm({ onSave }: { onSave: (plus: string, minus: string, next: string) => void }) {
  const [plus, setPlus] = useState("");
  const [minus, setMinus] = useState("");
  const [next, setNext] = useState("");
  return (
    <form onSubmit={(e) => { e.preventDefault(); onSave(plus, minus, next); }} className="space-y-2">
      <input className="w-full rounded-lg border p-2" value={plus} onChange={(e) => setPlus(e.target.value)} placeholder="+ 잘한 점" />
      <input className="w-full rounded-lg border p-2" value={minus} onChange={(e) => setMinus(e.target.value)} placeholder="− 아쉬운 점" />
      <input className="w-full rounded-lg border p-2" value={next} onChange={(e) => setNext(e.target.value)} placeholder="⇒ 내일 할 것" />
      <div className="pt-2 text-right">
        <button className="rounded-xl bg-yellow-500 px-4 py-2 text-white hover:bg-yellow-600" type="submit">저장</button>
      </div>
    </form>
  );
}

function Timeline({ oft, pmn }: { oft: { date: string; output: string; focus: string; teach: string }[]; pmn: { date: string; plus: string; minus: string; next: string }[] }) {
  return (
    <div className="max-h-[60vh] overflow-y-auto space-y-4">
      <div>
        <h4 className="mb-2 font-semibold">OFT 기록</h4>
        <ul className="space-y-2 text-sm">
          {oft.length === 0 && <li className="text-gray-500">기록 없음</li>}
          {oft.map((o, i) => (
            <li key={`oft-${i}`} className="rounded-lg border p-2">
              <div className="text-xs text-gray-500">{o.date}</div>
              <div><span className="font-semibold">Output:</span> {o.output}</div>
              <div><span className="font-semibold">Focus:</span> {o.focus}</div>
              <div><span className="font-semibold">Teach:</span> {o.teach}</div>
            </li>
          ))}
        </ul>
      </div>
      <div>
        <h4 className="mb-2 font-semibold">PMN 성찰</h4>
        <ul className="space-y-2 text-sm">
          {pmn.length === 0 && <li className="text-gray-500">기록 없음</li>}
          {pmn.map((p, i) => (
            <li key={`pmn-${i}`} className="rounded-lg border p-2">
              <div className="text-xs text-gray-500">{p.date}</div>
              <div><span className="font-semibold">+</span> {p.plus}</div>
              <div><span className="font-semibold">−</span> {p.minus}</div>
              <div><span className="font-semibold">⇒</span> {p.next}</div>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

function GHCBoard({ goal, habit, challenge, pmnCount }: { goal: string; habit: string; challenge: string; pmnCount: number }) {
  return (
    <div className="space-y-3 text-sm">
      <div className="rounded-xl border p-3">
        <div><span className="font-semibold">Goal:</span> {goal}</div>
        <div><span className="font-semibold">Habit:</span> {habit}</div>
        <div><span className="font-semibold">Challenge:</span> {challenge}</div>
      </div>
      <div className="rounded-xl border p-3">
        <div className="mb-1 font-semibold">이번 달 성찰 진행률(일 수)</div>
        <div>{pmnCount} / 30 (예시)</div>
      </div>
    </div>
  );
}

function ChallengeForm({ current, onSave }: { current: { goal: string; habit: string; challenge: string }; onSave: (g: string, h: string, c: string) => void }) {
  const [g, setG] = useState(current.goal);
  const [h, setH] = useState(current.habit);
  const [c, setC] = useState(current.challenge);
  return (
    <form onSubmit={(e) => { e.preventDefault(); onSave(g, h, c); }} className="space-y-2">
      <input className="w-full rounded-lg border p-2" value={g} onChange={(e) => setG(e.target.value)} placeholder="장기 목표(Goal)" />
      <input className="w-full rounded-lg border p-2" value={h} onChange={(e) => setH(e.target.value)} placeholder="매일 습관(Habit)" />
      <input className="w-full rounded-lg border p-2" value={c} onChange={(e) => setC(e.target.value)} placeholder="월간 도전(Challenge)" />
      <div className="pt-2 text-right">
        <button className="rounded-xl bg-purple-600 px-4 py-2 text-white hover:bg-purple-700" type="submit">저장</button>
      </div>
    </form>
  );
}

/*
====================================
🧪 TESTS (Jest + React Testing Library) – 예시 시나리오 추가
------------------------------------
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import HomeScreen from './HomeScreen';

describe('HomeScreen 예시 동작', () => {
  test('기본 섹션 및 버튼 렌더링', () => {
    render(<HomeScreen />);
    expect(screen.getByText('GOP 프로젝트')).toBeInTheDocument();
    expect(screen.getByText('오늘의 Top 3 집중')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '편집' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '학습 시작하기' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '3줄 성찰 작성' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '보드 보기' })).toBeInTheDocument();
  });

  test('Top3 편집 모달에서 저장', () => {
    render(<HomeScreen />);
    fireEvent.click(screen.getByRole('button', { name: '편집' }));
    const input1 = screen.getByPlaceholderText('1번째 집중');
    fireEvent.change(input1, { target: { value: '수학 10문제' } });
    fireEvent.click(screen.getByRole('button', { name: '저장' }));
    expect(screen.getByText('수학 10문제')).toBeInTheDocument();
  });

  test('OFT 저장 후 타임라인 표시', () => {
    render(<HomeScreen />);
    fireEvent.click(screen.getByRole('button', { name: '학습 시작하기' }));
    fireEvent.change(screen.getByPlaceholderText('Output: 오늘 배운 것을 요약'), { target: { value: '분수 덧셈 이해' } });
    fireEvent.change(screen.getByPlaceholderText('Focus: 핵심 20%'), { target: { value: '통분 규칙' } });
    fireEvent.change(screen.getByPlaceholderText('Teach: 누군가에게 설명하듯 정리'), { target: { value: '친구에게 설명' } });
    fireEvent.click(screen.getByRole('button', { name: '저장' }));
    fireEvent.click(screen.getByRole('button', { name: '출력 기록 보기' }));
    expect(screen.getByText('분수 덧셈 이해')).toBeInTheDocument();
    expect(screen.getByText('통분 규칙')).toBeInTheDocument();
  });

  test('PMN 3줄 성찰 저장', () => {
    render(<HomeScreen />);
    fireEvent.click(screen.getByRole('button', { name: '3줄 성찰 작성' }));
    fireEvent.change(screen.getByPlaceholderText('+ 잘한 점'), { target: { value: '시간 지켜 공부' } });
    fireEvent.change(screen.getByPlaceholderText('− 아쉬운 점'), { target: { value: '집중력 흔들림' } });
    fireEvent.change(screen.getByPlaceholderText('⇒ 내일 할 것'), { target: { value: '25분 타이머 2회' } });
    fireEvent.click(screen.getByRole('button', { name: '저장' }));
    fireEvent.click(screen.getByRole('button', { name: '성찰 타임라인' }));
    expect(screen.getByText('시간 지켜 공부')).toBeInTheDocument();
    expect(screen.getByText('25분 타이머 2회')).toBeInTheDocument();
  });
});
====================================
*/

gop_app_home (1).jsx
0.02MB