제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. 최종 테스트
- FastAPI 서버 시작:
uvicorn main:app --reload
- 브라우저(
Live Server
를 통해)에서index.html
을(를) 엽니다. - 전체 주기 확인:
- 함선 목록이 자동으로 로드되어야 합니다.
- "새 장치 시작"을 클릭하고 양식을 작성한 후 저장합니다. 새 함선이 목록에 나타나는지 확인하십시오.
- 아무 함선에서나 "수정"을 클릭하고 데이터를 변경한 후 저장합니다. 정보가 업데이트되었는지 확인하십시오.
- 아무 함선에서나 "폐기"를 클릭하고 작업을 확인합니다. 목록에서 사라지는지 확인하십시오.
- 모든 오류 시나리오(잘못된 데이터, 서버 중지)를 확인하십시오.
복습 퀴즈
🚀 챕터 요약
귀하는 "비행 제어 센터"의 구축 및 출시를 성공적으로 완료했습니다.
- 🖥️ 구조화되고 스타일이 적용된 HTML/CSS 인터페이스를 만들었습니다.
- ⚙️ 전체 CRUD 주기를 구현하는 깔끔하고 모듈식 JavaScript 코드를 작성했습니다.
- 🛰️ 이제 귀하의 프런트엔드는 FastAPI로 생성된 백엔드를 완벽하게 제어할 수 있습니다.
챕터 4를 성공적으로 완료하신 것을 축하드립니다! 간단한 fetch
요청 전송부터 자체 API와 상호 작용하는 완전한 웹 애플리케이션 생성까지 모든 과정을 거쳤습니다.
```