๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu์˜ ์Šค์บํด๋”ฉ ์‹œ์Šคํ…œ์œผ๋กœ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์˜ ๋ชฉ๋ก๊ณผ ํผ ๋ทฐ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

View Scaffolding ๊ฐœ์š”

์ž๋™ ์ƒ์„ฑ

๋ชฉ๋ก/ํผ ๋ทฐ์›น UI๋กœ ์ƒ์„ฑ

ํƒ€์ž… ์•ˆ์ „

Entity ๊ธฐ๋ฐ˜์ปดํŒŒ์ผ ํƒ€์ž„ ๊ฒ€์ฆ

์ปค์Šคํ„ฐ๋งˆ์ด์ง•

์ƒ์„ฑ ํ›„ ์ˆ˜์ •์™„์ „ํ•œ ์ œ์–ด

์ผ๊ด€์„ฑ

ํ†ต์ผ๋œ UI/UX๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

View Scaffolding์ด๋ž€?

๋ฌธ์ œ: ๋ฐ˜๋ณต์ ์ธ CRUD ๋ทฐ ์ž‘์„ฑ

์ „ํ†ต์ ์ธ ๊ฐœ๋ฐœ์—์„œ๋Š” ๊ฐ ์—”ํ‹ฐํ‹ฐ๋งˆ๋‹ค ๋น„์Šทํ•œ CRUD ํ™”๋ฉด์„ ์ˆ˜๋™์œผ๋กœ ๋ฐ˜๋ณต ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜๋™ ์ž‘์„ฑ ์‹œ ๋ฌธ์ œ์ :
  1. ์‹œ๊ฐ„ ๋‚ญ๋น„: ๋ชฉ๋ก, ์ƒ์„ฑ, ์ˆ˜์ • ํŽ˜์ด์ง€๋ฅผ ๋ชจ๋‘ ์†์œผ๋กœ ์ž‘์„ฑ
  2. ์ผ๊ด€์„ฑ ๋ถ€์กฑ: ๊ฐœ๋ฐœ์ž๋งˆ๋‹ค ๋‹ค๋ฅธ ์Šคํƒ€์ผ, ๋‹ค๋ฅธ ๊ตฌ์กฐ
  3. ์œ ์ง€๋ณด์ˆ˜ ์–ด๋ ค์›€: ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ ์‹œ ๋ชจ๋“  ๋ทฐ ์ˆ˜์ • ํ•„์š”
  4. ์‹ค์ˆ˜ ๊ฐ€๋Šฅ์„ฑ: ํ•„๋“œ ๋ˆ„๋ฝ, ์ž˜๋ชป๋œ ํƒ€์ž…, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ˆ„๋ฝ
  5. ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ: ํ…Œ์ด๋ธ”, ํผ, ๋ฒ„ํŠผ ๋“ฑ ๋ฐ˜๋ณต ์ฝ”๋“œ
์˜ˆ์‹œ: User ์—”ํ‹ฐํ‹ฐ ํ•˜๋‚˜์— ํ•„์š”ํ•œ ํŽ˜์ด์ง€
/admin/users        โ†’ ๋ชฉ๋ก ํŽ˜์ด์ง€ (ํ…Œ์ด๋ธ”, ํŽ˜์ด์ง€๋„ค์ด์…˜, ๊ฒ€์ƒ‰)
/admin/users/form   โ†’ ์ƒ์„ฑ/์ˆ˜์ • ํŽ˜์ด์ง€ (ํผ, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ)
๊ฐ ํŽ˜์ด์ง€๋งˆ๋‹ค 100~200์ค„ ์ด์ƒ์˜ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•˜๊ณ , ์ด๋ฅผ ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ์— ๋ฐ˜๋ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ: ์ž๋™ ์Šค์บํด๋”ฉ

Sonamu๋Š” Entity ์ •์˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋ทฐ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์žฅ์ :
  1. โœจ ์ฆ‰์‹œ ์ƒ์„ฑ: ์›น UI๋กœ ํด๋ฆญ๋งŒ์œผ๋กœ ๋ทฐ ์ƒ์„ฑ
  2. โœจ ํƒ€์ž… ์•ˆ์ „: Entity ํƒ€์ž…์ด ์ปดํฌ๋„ŒํŠธ์— ๊ทธ๋Œ€๋กœ ๋ฐ˜์˜
  3. โœจ ์ผ๊ด€๋œ UI: ํ†ต์ผ๋œ ๋””์ž์ธ ์‹œ์Šคํ…œ
  4. โœจ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค: ๊ฒ€์ฆ๋œ ํŒจํ„ด ์ ์šฉ
  5. โœจ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅ: ์ƒ์„ฑ ํ›„ ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์ˆ˜์ •
Sonamu์˜ ์Šค์บํด๋”ฉ ์ฒ ํ•™:
โ€œ80%๋Š” ์ž๋™์œผ๋กœ, 20%๋Š” ์ปค์Šคํ„ฐ๋งˆ์ด์ง•โ€
๋Œ€๋ถ€๋ถ„์˜ CRUD ๊ธฐ๋Šฅ์€ ์ž๋™ ์ƒ์„ฑํ•˜๊ณ , ํŠน๋ณ„ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋งŒ ์ถ”๊ฐ€๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

์Šค์บํด๋”ฉ ์‹คํ–‰

Sonamu ์›น UI ์‚ฌ์šฉ

Sonamu๋Š” ์›น ๊ธฐ๋ฐ˜ ๊ด€๋ฆฌ UI๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
# Sonamu ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰
pnpm dev

# ๋ธŒ๋ผ์šฐ์ €์—์„œ Sonamu UI ์ ‘์†
# http://localhost:3000/sonamu
์›น UI์—์„œ ๋ทฐ ์ƒ์„ฑ:
  1. Sonamu UI ์ ‘์†
  2. โ€œScaffoldโ€ ๋ฉ”๋‰ด ์„ ํƒ
  3. Entity ์„ ํƒ (์˜ˆ: User)
  4. โ€œGenerate View Listโ€ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ index.tsx ์ƒ์„ฑ
  5. โ€œGenerate View Formโ€ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ form.tsx ์ƒ์„ฑ
CLI ๋ช…๋ น์–ด๋Š” ํ˜„์žฌ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ทฐ ์ƒ์„ฑ์€ Sonamu ์›น UI๋ฅผ ํ†ตํ•ด์„œ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์ƒ์„ฑ๋˜๋Š” ํŒŒ์ผ ๊ตฌ์กฐ

web/src/routes/admin/
โ””โ”€โ”€ users/
    โ”œโ”€โ”€ index.tsx    # ๋ชฉ๋ก ํŽ˜์ด์ง€ (ํ…Œ์ด๋ธ” ํฌํ•จ)
    โ””โ”€โ”€ form.tsx     # ์ƒ์„ฑ/์ˆ˜์ • ํ†ตํ•ฉ ํผ
์‹ค์ œ ์ƒ์„ฑ๋˜๋Š” ํŒŒ์ผ:
  • index.tsx: ๋ชฉ๋ก ํ…Œ์ด๋ธ”์ด ์ง์ ‘ ํฌํ•จ๋œ ํŽ˜์ด์ง€
  • form.tsx: ์ƒ์„ฑ๊ณผ ์ˆ˜์ •์„ ํ•˜๋‚˜์˜ ํผ์œผ๋กœ ํ†ตํ•ฉ
์ฃผ์˜:
  • ๋ณ„๋„์˜ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ(UserTable.tsx, UserForm.tsx)์€ ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ
  • ์ƒ์„ธ ํŽ˜์ด์ง€([id].tsx)๋Š” ์ž๋™ ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ
  • ๋ชจ๋“  ๋กœ์ง์ด ๋‘ ๊ฐœ์˜ ํŽ˜์ด์ง€ ํŒŒ์ผ์— ํ†ตํ•ฉ๋จ

์ƒ์„ฑ๋œ ๋ทฐ ๊ตฌ์กฐ

๋ชฉ๋ก ํŽ˜์ด์ง€ (index.tsx)

๋ชฉ๋ก์„ ํ…Œ์ด๋ธ”๋กœ ํ‘œ์‹œํ•˜๊ณ  ํŽ˜์ด์ง€๋„ค์ด์…˜, ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
// web/src/routes/admin/users/index.tsx (์ž๋™ ์ƒ์„ฑ)
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { UserService } from "@/services/services.generated";

export default function UsersPage() {
  const navigate = useNavigate();
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState("");
  
  // TanStack Query Hook ์‚ฌ์šฉ
  const { data: users, isLoading } = UserService.useUsers("A", {
    page,
    pageSize: 20,
    search,
  });
  
  return (
    <div className="admin-page">
      <div className="header">
        <h1>Users</h1>
        <button onClick={() => navigate("/admin/users/form")}>
          Create New User
        </button>
      </div>
      
      <div className="search">
        <input
          type="text"
          placeholder="Search users..."
          value={search}
          onChange={(e) => setSearch(e.target.value)}
        />
      </div>
      
      {/* ํ…Œ์ด๋ธ”์ด ์ง์ ‘ ํฌํ•จ๋จ */}
      <table className="data-table">
        <thead>
          <tr>
            <th>ID</th>
            <th>Email</th>
            <th>Username</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {users?.map((user) => (
            <tr key={user.id}>
              <td>{user.id}</td>
              <td>{user.email}</td>
              <td>{user.username}</td>
              <td>
                <button onClick={() => navigate(`/admin/users/form?id=${user.id}`)}>
                  Edit
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      
      {/* ํŽ˜์ด์ง€๋„ค์ด์…˜ */}
      <div className="pagination">
        <button disabled={page === 1} onClick={() => setPage(page - 1)}>
          Previous
        </button>
        <span>Page {page}</span>
        <button onClick={() => setPage(page + 1)}>
          Next
        </button>
      </div>
    </div>
  );
}
๊ธฐ๋Šฅ:
  • ํŽ˜์ด์ง€๋„ค์ด์…˜
  • ๊ฒ€์ƒ‰ ํ•„ํ„ฐ
  • โ€œCreate Newโ€ ๋ฒ„ํŠผ
  • โ€œEditโ€ ๋ฒ„ํŠผ (๊ฐ ํ–‰)
  • ํ…Œ์ด๋ธ”์ด ํŽ˜์ด์ง€์— ์ง์ ‘ ํฌํ•จ (๋ณ„๋„ ์ปดํฌ๋„ŒํŠธ ์—†์Œ)

ํผ ํŽ˜์ด์ง€ (form.tsx)

์ƒ์„ฑ๊ณผ ์ˆ˜์ •์„ ํ•˜๋‚˜์˜ ํผ์œผ๋กœ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.
// web/src/routes/admin/users/form.tsx (์ž๋™ ์ƒ์„ฑ)
import { useState, useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { UserService } from "@/services/services.generated";

export default function UserFormPage() {
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();
  const userId = searchParams.get("id");
  
  const [formData, setFormData] = useState({
    email: "",
    username: "",
    bio: "",
  });
  
  const [saving, setSaving] = useState(false);
  
  // ์ˆ˜์ • ๋ชจ๋“œ: ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋กœ๋“œ
  useEffect(() => {
    if (userId) {
      UserService.getUser("C", parseInt(userId)).then((user) => {
        setFormData({
          email: user.email,
          username: user.username,
          bio: user.bio || "",
        });
      });
    }
  }, [userId]);
  
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setSaving(true);
    
    try {
      if (userId) {
        // ์ˆ˜์ •
        await UserService.update(parseInt(userId), formData);
      } else {
        // ์ƒ์„ฑ
        await UserService.create(formData);
      }
      
      navigate("/admin/users");
    } catch (error) {
      alert("Failed to save");
    } finally {
      setSaving(false);
    }
  }
  
  return (
    <div className="admin-page">
      <div className="header">
        <h1>{userId ? "Edit User" : "Create New User"}</h1>
        <button onClick={() => navigate("/admin/users")}>
          Back to List
        </button>
      </div>
      
      <form onSubmit={handleSubmit} className="entity-form">
        <div className="form-field">
          <label>Email *</label>
          <input
            type="email"
            value={formData.email}
            onChange={(e) => setFormData({ ...formData, email: e.target.value })}
            required
          />
        </div>
        
        <div className="form-field">
          <label>Username *</label>
          <input
            type="text"
            value={formData.username}
            onChange={(e) => setFormData({ ...formData, username: e.target.value })}
            required
          />
        </div>
        
        <div className="form-field">
          <label>Bio</label>
          <textarea
            value={formData.bio}
            onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
            rows={4}
          />
        </div>
        
        <div className="form-actions">
          <button type="submit" disabled={saving}>
            {saving ? "Saving..." : "Save"}
          </button>
        </div>
      </form>
    </div>
  );
}
๊ธฐ๋Šฅ:
  • ์ƒ์„ฑ/์ˆ˜์ • ํ†ตํ•ฉ: URL ํŒŒ๋ผ๋ฏธํ„ฐ(?id=)๋กœ ๋ชจ๋“œ ๊ตฌ๋ถ„
  • ์ˆ˜์ • ์‹œ ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ž๋™ ๋กœ๋“œ
  • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
  • ์ €์žฅ ์ค‘ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”
  • โ€œBack to Listโ€ ๋ฒ„ํŠผ

์ƒ์„ฑ ํŒŒ์ผ์˜ ํŠน์ง•

ํ†ตํ•ฉ ๊ตฌ์กฐ

Sonamu์˜ ์Šค์บํด๋”ฉ์€ ์ตœ์†Œํ•œ์˜ ํŒŒ์ผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค: ์ „ํ†ต์ ์ธ ๊ตฌ์กฐ (๋ณต์žกํ•จ):
pages/users/
  โ”œโ”€โ”€ index.tsx
  โ”œโ”€โ”€ [id].tsx
  โ”œโ”€โ”€ new.tsx
  โ””โ”€โ”€ [id]/edit.tsx
components/users/
  โ”œโ”€โ”€ UserTable.tsx
  โ”œโ”€โ”€ UserDetail.tsx
  โ””โ”€โ”€ UserForm.tsx
Sonamu ๊ตฌ์กฐ (๊ฐ„๊ฒฐํ•จ):
admin/users/
  โ”œโ”€โ”€ index.tsx    # ํ…Œ์ด๋ธ” ํฌํ•จ
  โ””โ”€โ”€ form.tsx     # ์ƒ์„ฑ/์ˆ˜์ • ํ†ตํ•ฉ
ํ†ตํ•ฉ ๊ตฌ์กฐ์˜ ์žฅ์ :
  • ํŒŒ์ผ ์ˆ˜ ์ตœ์†Œํ™”
  • ํŒŒ์ผ ๊ฐ„ ์ด๋™ ๊ฐ์†Œ
  • ์œ ์ง€๋ณด์ˆ˜ ๊ฐ„ํŽธ
  • ์ฝ”๋“œ ์‘์ง‘๋„ ๋†’์Œ

๋ผ์šฐํŒ… ํŒจํ„ด

GET  /admin/users           โ†’ index.tsx (๋ชฉ๋ก)
GET  /admin/users/form      โ†’ form.tsx  (์ƒ์„ฑ)
GET  /admin/users/form?id=1 โ†’ form.tsx  (์ˆ˜์ •)
URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ƒ์„ฑ/์ˆ˜์ • ๋ชจ๋“œ๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

์ปค์Šคํ„ฐ๋งˆ์ด์ง•

์ƒ์„ฑ ํ›„ ์ˆ˜์ •

์ƒ์„ฑ๋œ ํŒŒ์ผ์€ ์™„์ „ํžˆ ์ˆ˜์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
// web/src/routes/admin/users/index.tsx (์ปค์Šคํ„ฐ๋งˆ์ด์ง•)
export default function UsersPage() {
  // ์ถ”๊ฐ€: ์—ญํ• ๋ณ„ ํ•„ํ„ฐ
  const [role, setRole] = useState<string | null>(null);
  
  const { data: users } = UserService.useUsers("A", {
    page,
    pageSize: 20,
    search,
    role, // ์ปค์Šคํ…€ ํŒŒ๋ผ๋ฏธํ„ฐ
  });
  
  return (
    <div>
      {/* ์ถ”๊ฐ€: ์—ญํ•  ํ•„ํ„ฐ UI */}
      <select value={role || ""} onChange={(e) => setRole(e.target.value || null)}>
        <option value="">All Roles</option>
        <option value="admin">Admin</option>
        <option value="user">User</option>
      </select>
      
      {/* ๊ธฐ์กด ํ…Œ์ด๋ธ”... */}
    </div>
  );
}

๋ณ„๋„ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌ

ํ•„์š”ํ•˜๋ฉด ์ˆ˜๋™์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
// components/users/UserTable.tsx (์ˆ˜๋™ ์ƒ์„ฑ)
export function UserTable({ users }: { users: User[] }) {
  return (
    <table className="data-table">
      {/* ํ…Œ์ด๋ธ” ๋‚ด์šฉ */}
    </table>
  );
}

// admin/users/index.tsx (์ˆ˜์ •)
import { UserTable } from "@/components/users/UserTable";

export default function UsersPage() {
  const { data: users } = UserService.useUsers("A", { page, pageSize: 20 });
  
  return (
    <div>
      <UserTable users={users || []} />
    </div>
  );
}

์ƒ์„ธ ํŽ˜์ด์ง€ ์ถ”๊ฐ€

์ž๋™ ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š” ์ƒ์„ธ ํŽ˜์ด์ง€๋Š” ์ˆ˜๋™์œผ๋กœ ์ถ”๊ฐ€:
// admin/users/[id].tsx (์ˆ˜๋™ ์ƒ์„ฑ)
import { useParams } from "react-router-dom";
import { UserService } from "@/services/services.generated";

export default function UserDetailPage() {
  const { id } = useParams();
  const { data: user } = UserService.useUser("C", parseInt(id!));
  
  return (
    <div>
      <h1>{user?.username}</h1>
      <p>{user?.email}</p>
      <p>{user?.bio}</p>
    </div>
  );
}

๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

1. ์ƒ์„ฑ ํ›„ ์ฆ‰์‹œ Git ์ปค๋ฐ‹

# Sonamu UI์—์„œ ๋ทฐ ์ƒ์„ฑ
git add web/src/routes/admin/users/
git commit -m "feat: scaffold User views"
์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๋‚ด์—ญ์„ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2. ๊ณตํ†ต ๋กœ์ง์€ Hook์œผ๋กœ ์ถ”์ถœ

// hooks/useEntityList.ts
export function useEntityList<T>(
  entityName: string,
  useHook: any
) {
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState("");
  
  const { data, isLoading } = useHook("A", {
    page,
    pageSize: 20,
    search,
  });
  
  return { data, isLoading, page, setPage, search, setSearch };
}

3. ์Šคํƒ€์ผ์€ ์ „์—ญ์œผ๋กœ ๊ด€๋ฆฌ

/* styles/admin.css */
.admin-page { }
.data-table { }
.entity-form { }
๋ชจ๋“  ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์—์„œ ์ผ๊ด€๋œ ์Šคํƒ€์ผ ์‚ฌ์šฉ.

์ฃผ์˜์‚ฌํ•ญ

View Scaffolding ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ์›น UI์—์„œ๋งŒ ์ƒ์„ฑ ๊ฐ€๋Šฅ (CLI ๋ช…๋ น์–ด ๋น„ํ™œ์„ฑํ™”๋จ)
  2. index.tsx, form.tsx๋งŒ ์ƒ์„ฑ (๋ณ„๋„ ์ปดํฌ๋„ŒํŠธ ์—†์Œ)
  3. ์ƒ์„ธ ํŽ˜์ด์ง€๋Š” ์ˆ˜๋™ ์ถ”๊ฐ€ ํ•„์š”
  4. ์žฌ์ƒ์„ฑํ•˜๋ฉด ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์†์‹ค (Git ์ปค๋ฐ‹ ํ•„์ˆ˜)
  5. ์ƒ์„ฑ ํ›„ ํ•„์š”์— ๋”ฐ๋ผ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ ๊ถŒ์žฅ

๋‹ค์Œ ๋‹จ๊ณ„