Skip to content

제4.6장: API를 위한 간단한 인터페이스 구축

학습 시간: 1시간 15분


1. 최종 조립: "비행 관제 센터" 가동

우리는 모든 시스템을 개별적으로 학습했습니다: "안테나"(Fetch)를 설정하고, "명령"(GET, POST, DELETE)을 전송하는 방법을 배웠으며, "비상 프로토콜"(오류 처리)을 개발했습니다.

이제 모든 구성 요소를 하나로 모아 우리의 비행 관제 센터(ЦУП)를 가동할 때입니다 — 우주 함대 관리를 위한 완전하고 인터랙티브한 인터페이스입니다.

우리의 목표:

  • 통일되고, 깔끔하며, 이해하기 쉬운 인터페이스를 구축합니다.
  • 선박 생성, 표시, 업데이트, 삭제의 완전한 CRUD 주기를 구현합니다.
  • 사용자에게 시각적 피드백(로딩, 성공, 오류)을 추가합니다.

💡 우주 비유:

우리는 개별 테스트 콘솔에서 ЦУП의 메인 화면으로 넘어갑니다. 이 화면에는 한 명의 운영자가 수십 가지 다른 시스템 간에 전환할 필요 없이 전체 함대를 제어할 수 있도록 필요한 모든 버튼과 표시기가 있어야 합니다.


2. 인터페이스 설계: "대시보드"

우리는 더 구조화된 HTML이 필요합니다. 선박을 표시하기 위해 "카드"를 사용하고, 편집을 위해 모달 창을 사용할 것입니다.

index.html 업데이트:

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>ЦУП v2.0 - 함대 관리</title>
    <link rel="stylesheet" href="style.css"> <!-- 스타일 연결 -->
</head>
<body>
    <header>
        <h1>우주 함대 제어 패널</h1>
    </header>

    <main>
        <section id="fleet-controls">
            <button id="load-fleet-btn">함대 목록 새로고침</button>
            <button id="show-create-form-btn">새 장치 발사</button>
        </section>

        <section id="fleet-display">
            <h2>현재 함대 구성</h2>
            <div id="fleet-list" class="cards-container">
                <!-- 선박 카드들이 여기에 표시됩니다 -->
            </div>
        </section>
    </main>

    <!-- 생성/편집을 위한 모달 창 (초기에는 숨김) -->
    <div id="modal" class="modal-overlay" style="display: none;">
        <div class="modal-content">
            <h2 id="modal-title">새 장치 발사</h2>
            <form id="ship-form">
                <input type="hidden" id="ship-id">
                <input type="text" id="ship-name" placeholder="이름" required>
                <input type="text" id="ship-type" placeholder="유형" required>
                <input type="number" id="ship-year" placeholder="발사 연도" required>
                <input type="text" id="ship-status" placeholder="상태" required>
                <div class="modal-actions">
                    <button type="submit" id="save-btn">저장</button>
                    <button type="button" id="cancel-btn">취소</button>
                </div>
            </form>
            <div id="notification-area"></div>
        </div>
    </div>

    <script src="api.js"></script>
    <script src="app.js"></script>
</body>
</html>


3. "우주" 디자인 추가: style.css

우리의 ЦУП이 멋지게 보이도록 style.css 파일을 만드세요.

/* style.css */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #1a1a2e;
    color: #e0e0e0;
    margin: 0;
    padding: 20px;
}
header { text-align: center; margin-bottom: 20px; }
button {
    background-color: #4a4e69;
    color: white;
    border: none;
    padding: 10px 15px;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}
button:hover { background-color: #6a6e94; }
.cards-container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
}
.card {
    background-color: #2a2a4e;
    border: 1px solid #4a4e69;
    border-radius: 8px;
    padding: 15px;
}
.card h3 { margin-top: 0; color: #9394a5; }
.card-actions { margin-top: 15px; }

/* 모달 창 스타일 */
.modal-overlay {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background-color: rgba(0,0,0,0.7);
    display: flex;
    justify-content: center;
    align-items: center;
}
.modal-content {
    background: #1a1a2e;
    padding: 20px;
    border-radius: 8px;
    border: 1px solid #4a4e69;
    width: 90%;
    max-width: 500px;
}
#ship-form input {
    width: calc(100% - 20px);
    padding: 10px;
    margin-bottom: 10px;
    border-radius: 4px;
    border: 1px solid #4a4e69;
    background-color: #2a2a4e;
    color: white;
}
.modal-actions { text-align: right; }


4. 논리 전체 재구성: app.js

이제 우리는 모든 지식을 통합하여 app.js의 최종 버전을 작성할 것입니다.

// app.js

// --- DOM 요소 ---
const loadFleetBtn = document.getElementById('load-fleet-btn');
const fleetListContainer = document.getElementById('fleet-list');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modal-title');
const shipForm = document.getElementById('ship-form');
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('cancel-btn');
const showCreateFormBtn = document.getElementById('show-create-form-btn');
const notificationArea = document.getElementById('notification-area');

// --- UI 함수 ---

function showNotification(message, isError = false) {
    notificationArea.textContent = message;
    notificationArea.style.color = isError ? '#ff6b6b' : '#6bff6b';
}

function openModalForCreate() {
    shipForm.reset();
    document.getElementById('ship-id').value = '';
    modalTitle.textContent = '새 장치 시작';
    modal.style.display = 'flex';
}

function openModalForEdit(ship) {
    shipForm.reset();
    document.getElementById('ship-id').value = ship.id;
    document.getElementById('ship-name').value = ship.name;
    document.getElementById('ship-type').value = ship.type;
    document.getElementById('ship-year').value = ship.launch_year;
    document.getElementById('ship-status').value = ship.status;
    modalTitle.textContent = `편집: ${ship.name}`;
    modal.style.display = 'flex';
}

function closeModal() {
    modal.style.display = 'none';
    notificationArea.textContent = '';
}

function createShipCard(ship) {
    const card = document.createElement('div');
    card.className = 'card';
    card.innerHTML = `
        <h3>${ship.name} (ID: ${ship.id})</h3>
        <p>유형: ${ship.type}</p>
        <p>출시 연도: ${ship.launch_year}</p>
        <p>상태: ${ship.status}</p>
        <div class="card-actions">
            <button class="edit-btn" data-ship-id="${ship.id}">수정</button>
            <button class="delete-btn" data-ship-id="${ship.id}">폐기</button>
        </div>
    `;
    return card;
}

// --- API 및 표시 로직 ---

async function fetchAndDisplayFleet() {
    try {
        fleetListContainer.innerHTML = '<p>텔레메트리 로드 중...</p>';
        const ships = await apiRequest('/spaceships');

        fleetListContainer.innerHTML = '';
        if (ships.length === 0) {
            fleetListContainer.innerHTML = '<p>등록된 장치가 없습니다.</p>';
            return;
        }
        ships.forEach(ship => {
            const card = createShipCard(ship);
            fleetListContainer.appendChild(card);
        });
    } catch (error) {
        fleetListContainer.innerHTML = `<p style="color: #ff6b6b;">플릿 로드 오류: ${error.message}</p>`;
    }
}

async function handleSaveShip(event) {
    event.preventDefault();
    const shipId = document.getElementById('ship-id').value;
    const shipData = {
        name: document.getElementById('ship-name').value,
        type: document.getElementById('ship-type').value,
        launch_year: parseInt(document.getElementById('ship-year').value),
        status: document.getElementById('ship-status').value
    };

    try {
        let response;
        if (shipId) {
            // 업데이트 (PUT)
            response = await apiRequest(`/spaceships/${shipId}`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(shipData)
            });
            showNotification(`장치 "${response.name}"이(가) 성공적으로 업데이트되었습니다!`);
        } else {
            // 생성 (POST)
            response = await apiRequest('/spaceships', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(shipData)
            });
            showNotification(`장치 "${response.name}"이(가) 성공적으로 시작되었습니다! ID: ${response.id}`);
        }

        setTimeout(() => {
            closeModal();
            fetchAndDisplayFleet();
        }, 1500);

    } catch (error) {
        showNotification(error.message, true);
    }
}

async function handleDeleteShip(shipId) {
    if (!confirm(`ID ${shipId}를(을) 가진 장치를 폐기하시겠습니까?`)) return;

    try {
        await apiRequest(`/spaceships/${shipId}`, { method: 'DELETE' });
        alert('장치가 성공적으로 폐기되었습니다.');
        fetchAndDisplayFleet();
    } catch (error) {
        alert(`폐기 중 오류 발생: ${error.message}`);
    }
}

// --- 이벤트 핸들러 ---

document.addEventListener('DOMContentLoaded', fetchAndDisplayFleet);
loadFleetBtn.addEventListener('click', fetchAndDisplayFleet);
showCreateFormBtn.addEventListener('click', openModalForCreate);
cancelBtn.addEventListener('click', closeModal);
shipForm.addEventListener('submit', handleSaveShip);

fleetListContainer.addEventListener('click', async (event) => {
    const target = event.target;
    if (target.classList.contains('delete-btn')) {
        handleDeleteShip(target.dataset.shipId);
    }
    if (target.classList.contains('edit-btn')) {
        try {
            const ship = await apiRequest(`/spaceships/${target.dataset.shipId}`);
            openModalForEdit(ship);
        } catch (error) {
            alert(`편집을 위한 데이터를 로드하지 못했습니다: ${error.message}`);
        }
    }
});

5. 최종 테스트

  1. FastAPI 서버 시작: uvicorn main:app --reload
  2. 브라우저(Live Server를 통해)에서 index.html을(를) 엽니다.
  3. 전체 주기 확인:
    • 함선 목록이 자동으로 로드되어야 합니다.
    • "새 장치 시작"을 클릭하고 양식을 작성한 후 저장합니다. 새 함선이 목록에 나타나는지 확인하십시오.
    • 아무 함선에서나 "수정"을 클릭하고 데이터를 변경한 후 저장합니다. 정보가 업데이트되었는지 확인하십시오.
    • 아무 함선에서나 "폐기"를 클릭하고 작업을 확인합니다. 목록에서 사라지는지 확인하십시오.
    • 모든 오류 시나리오(잘못된 데이터, 서버 중지)를 확인하십시오.

복습 퀴즈

1. 웹 인터페이스의 모달 창은...

2. `DOMContentLoaded` 이벤트는 다음의 경우에 발생합니다...

3. 최종 버전에서 생성과 편집에 하나의 양식을 사용하는 이유는 무엇입니까?

4. `data-ship-id="${ship.id}"`은(는) 다음의 예시입니다...

5. 코드 리팩토링(예: `api.js`로 로직 분리)은 다음을 위해 필요합니다...


🚀 챕터 요약

귀하는 "비행 제어 센터"의 구축 및 출시를 성공적으로 완료했습니다.

  • 🖥️ 구조화되고 스타일이 적용된 HTML/CSS 인터페이스를 만들었습니다.
  • ⚙️ 전체 CRUD 주기를 구현하는 깔끔하고 모듈식 JavaScript 코드를 작성했습니다.
  • 🛰️ 이제 귀하의 프런트엔드는 FastAPI로 생성된 백엔드를 완벽하게 제어할 수 있습니다.

챕터 4를 성공적으로 완료하신 것을 축하드립니다! 간단한 fetch 요청 전송부터 자체 API와 상호 작용하는 완전한 웹 애플리케이션 생성까지 모든 과정을 거쳤습니다. ```