카테고리 없음

lovable.dev로 나만의 홈페이지 만들어 보기

mapleaiclub 2025. 5. 25. 15:11

코딩을 하나도 모르는, 코딩 문외한이 AI를 사용해서 나만의 홈페이지를 만들어보았습니다.
기본적으로 GPT를 사용해서 프롬프트, 기본적인 구조를 짰으며 lovable.dev를 이용하여 홈페이지를 제작해 보았습니다.
 
 
 

5월 22일 목요일... 아직도 잊히지 않는 토트넘 우승의 순간입니다.
홈페이지 생성 AI에 관한 정보를 접해 감동적이었던 우승 순간을 담는 홈페이지를 만들어보자 하여 해당 주제를 선정하였습니다.
 
ⓛ GPT를 활용하여 주제와 관련된 정보 수집
 
기본적으로, 홈페이지 생성 AI에 필요한 정보를 주입하여 생성하기 위해서는 맞춤형 정보가 필요하다고 생각했습니다.
그래서 GPT o3 모델의 웹검색 을 활용하여
기본적인 정보를 수집하게 했습니다.

이상한 정보가 껴있는 경우도 많아서 반복하여 질문했습니다.

 
처음에는 아레나에서 홈페이지를 제작해보았습니다.
이하는 반복하여 만들어진 프롬포트 입니다. 중간에 코드도 섞어서 최종 프롬프트를 확정시켰습니다.

# 목표
“토트넘 핫스퍼 2024-25 유로파리그 우승 여정” 웹사이트를 Next.js 14(App Router) + React 18 + Tailwind v3로 구현한다.  
모든 섹션(A~H)과 텍스트·데이터를 단 한 글자도 생략하지 않는다.

# 1. 의존성
- framer-motion
- gsap

# 2. 폴더 구조
.
├─ app/
│  └─ page.tsx ← ★아래 코드로 덮어쓰기
├─ components/
│  └─ TrophyScene.tsx ← Three.js 임시 컴포넌트(예: 회전하는 큐브) 직접 작성
├─ public/
│  ├─ sammames_ceremony.jpg
│  ├─ vanderven_clear.gif
│  ├─ vicario_save.jpg
│  ├─ footer_trophy_lift.jpg
│  ├─ cards/
│  │   ├─ solanke.png
│  │   ├─ romero.png
│  │   ├─ vicario.png
│  │   └─ porro.png
│  └─ gallery/
│      ├─ johnson_goal.jpg
│      ├─ vanderven_clear.jpg
│      ├─ solanke_pk.jpg
│      ├─ porro_goal.jpg
│      ├─ son_run.jpg
│      └─ team_celebrate.jpg
└─ tailwind.config.js ← GOLD 색상(#FDB913) 추가

# 3. tailwind.config.js 수정 예시
export default {
  darkMode: "media",
  theme: {
    extend: {
      colors: { gold: "#FDB913" }
    },
  },
  plugins: [],
};

# 4. app/page.tsx 전체 코드
```tsx
// app/page.tsx ─ Next.js 14 (App Router) + React 18 + Tailwind v3
"use client";

import React from "react";
import dynamic from "next/dynamic";
import Image from "next/image";
import { motion } from "framer-motion";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

/* ──────────────────────────────────────────
   0. 전역 상수 · 데이터
   ────────────────────────────────────────── */
const NAVY  = "#001C42";
const WHITE = "#FFFFFF";
const GOLD  = "#FDB913";

/** B. 리그 단계 8경기 타임라인 */
const timelineMatches = [
  { id: 1, date: "2024-09-26", opp: "카라바흐", score: "3-0", scorers: "12′ 존슨, 55′ 솔란케, 78′ 포로", },
  { id: 2, date: "2024-10-03", opp: "페렌츠바로시", score: "2-1", scorers: "22′ P.사르, 71′ 존슨 / 83′ 스쿼르카", },
  { id: 3, date: "2024-10-24", opp: "AZ 알크마르", score: "1-0", scorers: "53′ 리샬리송 PK", },
  { id: 4, date: "2024-11-07", opp: "갈라타사라이", score: "2-3", scorers: "17′·43′ 오시멘, 80′ 악튀르코을루 / 35′ 랭크셔, 60′ 존슨", },
  { id: 5, date: "2024-11-28", opp: "로마", score: "2-2", scorers: "29′ 손흥민, 67′ 존슨 / 41′ 루카쿠, 90′ 훔멜스", },
  { id: 6, date: "2025-01-16", opp: "레인저스", score: "1-1", scorers: "38′ 캔트웰 / 74′ 쿨루셉스키", },
  { id: 7, date: "2025-01-23", opp: "호펜하임", score: "3-2", scorers: "31′ 크라마리치, 90′ 베흐호르스트 / 18′·72′ 손흥민, 55′ 매디슨", },
  { id: 8, date: "2025-01-30", opp: "엘프스보리", score: "3-0", scorers: "44′ 스칼렛, 67′ 베리발, 90+4′ 무어", },
];

/** C. 토너먼트 브래킷 */
const bracket = [
  { round: "16강", agg: "3-2", opp: "AZ 알크마르", notes: "0-1 A / 3-1 H" },
  { round: "8강", agg: "2-1", opp: "아인트라흐트 프랑크푸르트", notes: "1-1 H / 1-0 A" },
  { round: "4강", agg: "5-1", opp: "보되/글림트", notes: "3-1 H / 2-0 A" },
  { round: "결승", agg: "1-0", opp: "맨체스터 유나이티드", notes: "45′ 브레넌 존슨 발리골" },
];

/** D. 결승 매치팩트 */
const matchFacts = {
  possession: "35 %",
  shots: "3-15",
  onTarget: "1-4",
  keyMoments: [
    { id: 1, time: "45′",  title: "브레넌 존슨 결승골",        type: "youtube", src: "https://www.youtube.com/embed/ABCDE12345?modestbranding=1&rel=0", },
    { id: 2, time: "78′",  title: "미키 반더벤 골라인 클리어", type: "gif",     src: "/vanderven_clear.gif", },
    { id: 3, time: "90+4′", title: "비카리오 극적 세이브",     type: "img",     src: "/vicario_save.jpg", },
  ],
};

/** F-1. 득점 Top 5 */
const topScorers = [
  { name: "브레넌 존슨", goals: 5 },
  { name: "도미닉 솔란케", goals: 5 },
  { name: "손흥민",       goals: 3 },
  { name: "제임스 매디슨", goals: 3 },
  { name: "페드로 포로",   goals: 3 },
];
const totalEuroTitles = 1;

/** G. TOTS 4인 카드 */
const totsPlayers = [
  { id: 1, name: "도미닉 솔란케",     pos: "FW", rating: "8.1", stats: "5골 1도움 MOTM 2", img: "/cards/solanke.png", },
  { id: 2, name: "크리스티안 로메로", pos: "CB", rating: "8.3", stats: "0골 0도움 MOTM 3", img: "/cards/romero.png",  },
  { id: 3, name: "구글리엘모 비카리오", pos: "GK", rating: "8.5", stats: "클린시트 6회 세이브 34", img: "/cards/vicario.png", },
  { id: 4, name: "페드로 포로",       pos: "RB", rating: "7.9", stats: "3골 4도움 MOTM 1", img: "/cards/porro.png",   },
];

/* ──────────────────────────────────────────
   1. 유틸리티 컴포넌트
   ────────────────────────────────────────── */
const Section = ({ children, bg = WHITE, extra = "" }: { children: React.ReactNode; bg?: string; extra?: string; }) => (
  <section
    className={`relative ${extra}`}
    style={{ background: bg, clipPath: "polygon(0 0,100% 0,100% calc(100% - 4rem),0 100%)" }}
  >
    {children}
  </section>
);

/* Three.js 3D 트로피 ─ 상대 경로 import */
const TrophyScene = dynamic(() => import("../components/TrophyScene"), {
  ssr: false,
  loading: () => <div className="text-center text-white">Loading 3D…</div>,
});

/* ──────────────────────────────────────────
   2. 메인 컴포넌트
   ────────────────────────────────────────── */
export default function TottenhamJourney() {
  return (
    <main className="font-sans text-gray-900 dark:text-gray-100 bg-white dark:bg-[#001C42]">
      {/* A. 히어로 ------------------------------------------------ */}
      <Section bg={NAVY} extra="h-screen flex flex-col items-center justify-center text-center overflow-hidden">
        <Image src="/sammames_ceremony.jpg" alt="빌바오 산 마메스 트로피 세리머니" fill priority sizes="100vw" className="object-cover opacity-60 blur-md" />
        <div className="relative z-10 space-y-6">
          <TrophyScene />
          <h1 className="text-4xl md:text-6xl font-bold text-white">영광의 밤, 빌바오</h1>
          <p className="text-xl font-medium text-white">토트넘 핫스퍼 • 2025 유로파리그 챔피언</p>
        </div>
      </Section>

      {/* B. 리그 단계 타임라인 ---------------------------------- */}
      <Section bg={WHITE} extra="py-20">
        <h2 className="text-3xl font-bold text-center mb-12">리그 단계 타임라인</h2>
        <div className="flex gap-4 overflow-x-auto px-4 snap-x">
          {timelineMatches.map((m) => (
            <motion.div key={m.id} whileHover={{ y: -6 }} className="snap-center w-64 shrink-0 bg-[#001C42] text-white p-4 rounded-xl">
              <h3 className="font-semibold">{m.date}</h3>
              <p className="mt-2 text-lg">토 {m.score} {m.opp}</p>
              <p className="mt-1 text-sm opacity-90">{m.scorers}</p>
            </motion.div>
          ))}
        </div>
      </Section>

      {/* C. 토너먼트 브래킷 ------------------------------------- */}
      <Section bg={NAVY} extra="py-20 text-white">
        <h2 className="text-3xl font-bold text-center mb-12">토너먼트 브래킷</h2>
        <div className="max-w-md mx-auto border-l border-white pl-6 space-y-6">
          {bracket.map((b) => (
            <div key={b.round} className="relative">
              <span className="absolute -left-[6.5px] top-2 w-3 h-3 bg-[#FDB913] rounded-full" />
              <h3 className="font-semibold">{b.round} — 합계 {b.agg}</h3>
              <p className="text-sm opacity-90">{b.opp} ({b.notes})</p>
            </div>
          ))}
        </div>
      </Section>

      {/* D. 결승 매치팩트 ------------------------------------- */}
      <Section bg="#000" extra="py-20 text-white">
        <h2 className="text-3xl font-bold text-center mb-12">결승 매치팩트</h2>
        <div className="grid md:grid-cols-2 gap-12 max-w-5xl mx-auto">
          {/* 통계 */}
          <div className="space-y-4">
            <p className="text-lg">점유율 <span className="font-bold text-[#FDB913]">{matchFacts.possession}</span></p>
            <p className="text-lg">슈팅 <span className="font-bold text-[#FDB913]">{matchFacts.shots}</span></p>
            <p className="text-lg">유효슛 <span className="font-bold text-[#FDB913]">{matchFacts.onTarget}</span></p>
          </div>
          {/* 주요 장면 */}
          <div className="space-y-6">
            {matchFacts.keyMoments.map((e) => (
              <div key={e.id} className="space-y-2">
                <p className="font-semibold">{e.time} — {e.title}</p>
                {e.type === "youtube" ? (
                  <iframe className="w-full aspect-video rounded-lg" src={e.src} title={e.title} allowFullScreen />
                ) : (
                  <Image src={e.src} alt={e.title} width={640} height={360} className="rounded-lg" />
                )}
              </div>
            ))}
          </div>
        </div>
      </Section>

      {/* E. 하이라이트 갤러리 ---------------------------------- */}
      <Section bg={WHITE} extra="py-20">
        <h2 className="text-3xl font-bold text-center mb-12">하이라이트 갤러리</h2>
        <div className="columns-1 sm:columns-2 gap-4 px-4">
          {[
            { src: "/gallery/johnson_goal.jpg",       alt: "존슨 결승골" },
            { src: "/gallery/vanderven_clear.jpg",    alt: "반더벤 클리어" },
            { src: "/gallery/solanke_pk.jpg",         alt: "솔란케 PK" },
            { src: "/gallery/porro_goal.jpg",         alt: "포로 득점" },
            { src: "/gallery/son_run.jpg",            alt: "손흥민 질주" },
            { src: "/gallery/team_celebrate.jpg",     alt: "팀 세리머니" },
          ].map((g) => (
            <Image key={g.src} src={g.src} alt={g.alt} width={900} height={600} className="mb-4 rounded-lg break-inside-avoid" />
          ))}
        </div>
      </Section>

      {/* F. 팀 통계 ------------------------------------------- */}
      <Section bg={WHITE} extra="py-20">
        <h2 className="text-3xl font-bold text-center mb-12">팀 통계</h2>
        {/* 득점 Top 5 */}
        <div className="max-w-xl mx-auto">
          <h3 className="font-semibold mb-4">득점 Top 5</h3>
          <div className="space-y-3">
            {topScorers.map((s) => {
              const pct = (s.goals / topScorers[0].goals) * 100;
              return (
                <div key={s.name} className="flex items-center">
                  <span className="w-28 text-sm">{s.name}</span>
                  <div className="flex-1 mx-2 h-3 bg-gray-200 dark:bg-gray-700 rounded-full">
                    <motion.div style={{ width: `${pct}%` }} className="h-3 rounded-full" animate={{ backgroundColor: GOLD }} />
                  </div>
                  <span className="w-6 text-right font-medium">{s.goals}</span>
                </div>
              );
            })}
          </div>
        </div>
        {/* 카운트업 */}
        <div className="mt-16 text-center">
          <motion.p initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} transition={{ duration: 0.6 }} className="text-xl">
            유럽 대회 우승 →
            <motion.span initial={{ scale: 0, opacity: 0 }} whileInView={{ scale: 1, opacity: 1 }} transition={{ duration: 1.0, delay: 0.2 }}
              className="text-5xl font-extrabold text-[#FDB913] mx-2 inline-block">
              {totalEuroTitles}
            </motion.span>
            개
          </motion.p>
        </div>
      </Section>

      {/* G. TOTS 4인 카드 ------------------------------------- */}
      <Section bg={NAVY} extra="py-20 text-white">
        <h2 className="text-3xl font-bold text-center mb-12">유로파리그 올해의 팀 선수</h2>
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 px-4">
          {totsPlayers.map((p) => (
            <motion.div key={p.id} whileHover={{ rotateY: 180 }} transition={{ duration: 0.8 }} className="relative w-full h-72 [transform-style:preserve-3d] cursor-pointer">
              {/* 앞면 */}
              <div className="absolute inset-0 bg-[#0a2a54] rounded-xl flex flex-col items-center justify-center gap-4 backface-hidden">
                <Image src={p.img} alt={p.name} width={120} height={120} className="rounded-full border-2 border-[#FDB913]" />
                <p className="font-bold">{p.name}</p>
                <p className="text-sm opacity-80">{p.pos}</p>
                <p className="text-xl font-extrabold text-[#FDB913]">★ {p.rating}</p>
              </div>
              {/* 뒷면 */}
              <div className="absolute inset-0 bg-[#001C42] rounded-xl flex flex-col items-center justify-center gap-4 rotate-y-180 backface-hidden">
                <p className="text-center whitespace-pre-line px-4">{p.stats}</p>
              </div>
            </motion.div>
          ))}
        </div>
      </Section>

      {/* H. 푸터 ---------------------------------------------- */}
      <footer className="bg-black text-gray-300 text-center py-10">
        <div className="max-w-3xl mx-auto space-y-6">
          <p>데이터 출처: UEFA · ESPN • 제작: WebDev Arena</p>
          <Image src="/footer_trophy_lift.jpg" alt="토트넘 트로피 세리머니" width={960} height={540} className="mx-auto rounded-lg" />
          <a href="https://github.com/your-id/tottenham-europa-journey" target="_blank" rel="noopener noreferrer"
             className="inline-flex items-center gap-2 hover:text-[#FDB913]">
            <svg viewBox="0 0 16 16" fill="currentColor" className="w-5 h-5" aria-hidden>
              <path d="M8 0C3.58 0 0 3.58 0 ..."></path>
            </svg>
            GitHub
          </a>
        </div>
      </footer>
    </main>
  );
}

 
② lovable.dev로 홈페이지 제작
 
만들어진 프롬프트를 기반으로 https://lovable.dev/ 에서 프롬프트를 붙여 사이트를 생성했습니다.

 

스스로 코딩하고, 오류가 있는 경우 고쳐서 재생성합니다.

 
그렇게 해서 만들어진 홈페이지를 보니, 홈페이지 내에서도 오류가 있는 정보들도 있었고, 이미지와 동영상이 들어갈 곳이 임의로 아무 사진이나 들어가 있어서... 다시 명령어를 내려줬습니다.

생뚱맞은 사진들이 들어가 있습니다.

 

오류를 수정하고, 정확한 영상 링크 및 사진을 게시하였습니다.

 
몇 차례정도 걸치니 어느정도 원하는 홈페이지는 나왔으나.. 그 이후 발생하는 오류들은 수정하지 못했습니다.
하루에 한도가 5번 대화만 가능하여서 그랬습니다... 유료 결제를 할 경우 조금 더 많은 할당량으로 조정이 가능한 거 같네요. 

이미지와 영상이 제대로 들어가 있습니다.

 
 

하루 할당량 대화 5회 한도가 초과되었습니다..

 
AI를 활용해서 코딩을 모르는 사람도 약간의 프롬프트 조정만 해준다면, 홈페이지 개설도 가능하다는게 정말 신기했었습니다!