# Node.js 버전 확인
C:\Users\lucks>node -v
v20.13.1
# Node Version Manager(NVM)으로 Node.js 버전을 20.x로 변경 (이 경우 v20.4.0)
C:\Users\lucks>nvm use 20
Now using node v20.4.0 (64-bit)
# 새로운 React 프로젝트를 client 폴더에 생성
D:\Source\React>npx create-react-app client
# 생성된 client 폴더로 이동
D:\Source\React>cd client
# React 프로젝트에 antd(UI 라이브러리)와 axios(HTTP 클라이언트) 설치
D:\Source\React\client> npm i antd axios
# 새로운 .NET Core Web API 프로젝트를 UsersAPI라는 이름으로 생성
D:\Source\React>dotnet new webapi -n UsersAPI
# 생성된 UsersAPI 폴더로 이동
D:\Source\React>cd UsersAPI
# Entity Framework Core에서 SQLite를 사용하는 패키지 추가
D:\Source\React\UsersAPI>dotnet add package Microsoft.EntityFrameworkCore.Sqlite
# Entity Framework Core 툴 패키지 추가 (마이그레이션 및 DB 작업을 위해)
D:\Source\React\UsersAPI>dotnet add package Microsoft.EntityFrameworkCore.Tools
# .NET Core Web API 서버 실행
D:\Source\React\UsersAPI>dotnet run
Program.cs (ASP.NET CORE API 소스)
이 코드는 기본적인 CRUD (생성, 읽기, 업데이트, 삭제) 기능을 제공하는 ASP.NET Core API로, 데이터베이스로 SQLite를 사용하며, CORS 정책을 통해 여러 도메인에서의 접근을 허용합니다.
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args); // 애플리케이션 빌더 생성
// CORS(Cross-Origin Resource Sharing) 정책 설정
builder.Services.AddCors(options =>
options.AddDefaultPolicy(policy =>
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())); // 모든 Origin, Method, Header를 허용하는 기본 CORS 정책 추가
// SQLite 데이터베이스 설정 및 의존성 주입 등록
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite("Data Source=users.db")); // SQLite를 사용하여 "users.db" 데이터베이스 설정
// Swagger/OpenAPI 설정 (주석 처리됨)
// Swagger는 API 문서화를 위해 사용되는 도구
//builder.Services.AddEndpointsApiExplorer();
//builder.Services.AddSwaggerGen();
var app = builder.Build(); // 애플리케이션 빌드
// CORS 미들웨어 사용
app.UseCors();
// 데이터베이스 생성 (초기화)
// 앱이 실행될 때 데이터베이스가 존재하지 않으면 자동으로 생성
using (var scope = app.Services.CreateScope()) // 새 서비스 범위 생성
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); // DbContext 가져오기
db.Database.EnsureCreated(); // 데이터베이스가 없으면 생성
}
// GET: /api/users
// 전체 사용자 목록을 반환하는 엔드포인트
app.MapGet("/api/users", async (AppDbContext db) =>
await db.Users.ToListAsync()); // 데이터베이스에서 모든 사용자 데이터를 비동기적으로 가져와 반환
// GET: /api/users/{id}
// 특정 id에 해당하는 사용자 정보 반환
app.MapGet("/api/users/{id:int}", async (int id, AppDbContext db) =>
await db.Users.FindAsync(id) is User user ? Results.Ok(user) : Results.NotFound()); // id로 사용자를 찾고, 있으면 OK 응답, 없으면 NotFound 응답
// POST: /api/users
// 새로운 사용자를 추가하는 엔드포인트
app.MapPost("/api/users", async (User newUser, AppDbContext db) =>
{
db.Users.Add(newUser); // 새로운 사용자 추가
await db.SaveChangesAsync(); // 데이터베이스에 변경 사항 저장
return Results.Created($"/api/users/{newUser.Id}", newUser); // Created 응답 반환 (201 상태 코드)
});
// PUT: /api/users/{id}
// 기존 사용자의 정보를 수정하는 엔드포인트
app.MapPut("/api/users/{id:int}", async (int id, User updatedUser, AppDbContext db) =>
{
var user = await db.Users.FindAsync(id); // 주어진 id로 사용자를 찾음
if (user is null) return Results.NotFound(); // 사용자가 없으면 NotFound 반환
// 사용자 정보 업데이트
user.Name = updatedUser.Name;
user.Email = updatedUser.Email;
user.Password = updatedUser.Password;
await db.SaveChangesAsync(); // 데이터베이스에 변경 사항 저장
return Results.NoContent(); // NoContent 응답 반환 (204 상태 코드)
});
// DELETE: /api/users/{id}
// 특정 id의 사용자를 삭제하는 엔드포인트
app.MapDelete("/api/users/{id:int}", async (int id, AppDbContext db) =>
{
var user = await db.Users.FindAsync(id); // 주어진 id로 사용자를 찾음
if (user is null) return Results.NotFound(); // 사용자가 없으면 NotFound 반환
db.Users.Remove(user); // 사용자 삭제
await db.SaveChangesAsync(); // 데이터베이스에 변경 사항 저장
return Results.NoContent(); // NoContent 응답 반환 (204 상태 코드)
});
app.Run(); // 애플리케이션 실행
// User 모델 정의
public class User
{
public int Id { get; set; } // 사용자 고유 ID (Primary Key)
public string Name { get; set; } = string.Empty; // 사용자 이름
public string Email { get; set; } = string.Empty; // 사용자 이메일
public string Password { get; set; } = string.Empty; // 사용자 비밀번호
}
// AppDbContext: Entity Framework Core의 DbContext 클래스
// 데이터베이스와의 상호작용을 담당하는 클래스
public class AppDbContext : DbContext
{
public DbSet<User> Users => Set<User>(); // Users 테이블 정의
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } // DbContext 생성자
}
설명:
이 방식은 상태가 비동기적으로 처리되는 문제를 해결하며, Add User 버튼을 클릭할 때마다 폼이 항상 초기화된 상태로 나타나게 합니다.
import React, { useState, useEffect } from "react";
import { Layout, Menu, Table, Form, Input, Button, Modal, Space } from "antd";
import { PlusOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
import axios from "axios"; // HTTP 요청을 위한 axios 라이브러리
// Ant Design Layout 컴포넌트에서 사용되는 Header, Content, Footer
const { Header, Content, Footer } = Layout;
const { Search } = Input; // Ant Design의 Input에서 제공하는 Search 컴포넌트
const App = () => {
// 사용자 데이터 관리용 상태
const [users, setUsers] = useState([]);
// 모달(사용자 추가/수정) 표시 여부 관리 상태
const [isModalVisible, setIsModalVisible] = useState(false);
// 현재 편집 중인 사용자 정보 관리 상태
const [currentUser, setCurrentUser] = useState(null);
// 검색어를 관리하는 상태
const [searchTerm, setSearchTerm] = useState("");
const [form] = Form.useForm(); // Ant Design의 Form 인스턴스 생성
// 서버에서 사용자 데이터를 가져오는 비동기 함수
const fetchUsers = async () => {
const response = await axios.get("http://localhost:5209/api/users");
setUsers(response.data); // 가져온 데이터를 상태에 저장
};
// 사용자 추가 또는 편집 처리 함수
const handleAddOrEditUser = async (values) => {
if (currentUser) {
// 편집 모드인 경우 (currentUser가 null이 아님)
await axios.put(
`http://localhost:5209/api/users/${currentUser.id}`,
values
);
} else {
// 추가 모드인 경우 (currentUser가 null인 경우)
await axios.post(`http://localhost:5209/api/users`, values);
}
// 모달 닫기 및 상태 초기화
setIsModalVisible(false);
setCurrentUser(null);
form.resetFields(); // 폼 필드 초기화
fetchUsers(); // 사용자 목록을 다시 불러옴
};
// 사용자 삭제 처리 함수
const handleDeleteUser = async (id) => {
await axios.delete(`http://localhost:5209/api/users/${id}`);
fetchUsers(); // 사용자 목록을 다시 불러옴
};
// 컴포넌트가 마운트될 때 사용자 데이터를 불러오는 useEffect
useEffect(() => {
return () => {
fetchUsers(); // 초기 사용자 목록 로딩
};
}, []);
// currentUser 상태가 변경될 때마다 폼을 초기화하거나, 값을 설정
useEffect(() => {
if (!currentUser) {
form.resetFields(); // currentUser가 없으면 폼을 초기화
} else {
form.setFieldsValue(currentUser); // 편집 모드일 경우, 폼에 값을 설정
}
}, [currentUser, form]);
// 검색어에 맞게 사용자 목록을 필터링
const filteredUsers = users.filter(
(user) =>
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
);
// 테이블 컬럼 정의
const columns = [
{
title: "Name", // 컬럼 제목
dataIndex: "name", // users 데이터의 필드명
key: "name", // React에서 사용될 고유 키
},
{
title: "Email",
dataIndex: "email",
key: "email",
},
{
title: "Password",
dataIndex: "password",
key: "password",
},
{
title: "Action", // 수정/삭제 버튼이 들어갈 컬럼
key: "action",
render: (_, record) => (
<Space>
<Button
icon={<EditOutlined />} // 편집 아이콘
onClick={() => {
setCurrentUser(record); // 편집할 사용자를 currentUser로 설정
setIsModalVisible(true); // 모달 표시
}}
/>
<Button
icon={<DeleteOutlined />} // 삭제 아이콘
danger
onClick={() => handleDeleteUser(record.id)} // 삭제 함수 호출
/>
</Space>
),
},
];
// 비밀번호 입력 필드 유효성 검사 규칙
const passwordRules = [
{ required: true, message: "Password is required" }, // 필수 입력 항목
{ min: 8, message: "Password must be at least 8 characters" }, // 최소 8자 이상
{
pattern: /(?=.*[a-z])/,
message: "Password must contain at least one lowercase letter", // 소문자 포함
},
{
pattern: /(?=.*[A-Z])/,
message: "Password must contain at least one uppercase letter", // 대문자 포함
},
{
pattern: /(?=.*[0-9])/,
message: "Password must contain at least one number", // 숫자 포함
},
{
pattern: /(?=.*[!@#$%^&*])/,
message: "Password must contain at least one special character", // 특수 문자 포함
},
];
return (
<Layout style={{ minHeight: "100vh" }}>
<Header>
<Menu theme="dark" mode="horizontal">
<Menu.Item key="home">Home</Menu.Item>
<Menu.Item key="contact">Contacts</Menu.Item>
</Menu>
</Header>
<Content style={{ padding: "50px" }}>
{/* 사용자 추가 버튼 */}
<Space style={{ marginBottom: "20px", float: "left" }}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setCurrentUser(null); // 사용자 추가 모드로 전환 (currentUser를 null로 설정)
setIsModalVisible(true); // 모달 표시
}}
>
Add User
</Button>
</Space>
{/* 검색 필드 */}
<Space style={{ marginBottom: "20px", float: "right" }}>
<Search
placeholder="Search by name or email" // 검색 필드의 placeholder
onSearch={(value) => setSearchTerm(value)} // 검색어 설정
style={{ width: 300 }}
/>
</Space>
<div style={{ clear: "both" }} />
{/* 사용자 목록 테이블 */}
<Table dataSource={filteredUsers} columns={columns} rowKey="id" />
{/* 사용자 추가/편집 모달 */}
<Modal
title={currentUser ? "Edit User" : "Add User"} // 편집/추가 모드에 따라 모달 제목 변경
visible={isModalVisible} // 모달 표시 여부
onCancel={() => {
setIsModalVisible(false); // 모달 닫기
setCurrentUser(null); // currentUser 초기화
form.resetFields(); // 폼 필드 초기화
}}
footer={null} // 모달 하단에 버튼을 표시하지 않음
>
<Form
form={form} // 폼 인스턴스
onFinish={handleAddOrEditUser} // 제출 시 호출될 함수
layout="vertical" // 세로 레이아웃
>
{/* 이름 입력 필드 */}
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
{/* 이메일 입력 필드 */}
<Form.Item
name="email"
label="Email"
rules={[{ required: true, type: "email" }]}
>
<Input />
</Form.Item>
{/* 비밀번호 입력 필드 */}
<Form.Item name="password" label="Password" rules={passwordRules}>
<Input.Password />
</Form.Item>
{/* 제출 버튼 */}
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</Modal>
</Content>
{/* 페이지 하단에 푸터 */}
<Footer
style={{
textAlign: "center",
backgroundColor: "#001529",
color: "#fff",
position: "sticky",
bottom: 0,
}}
>
© {new Date().getFullYear()} Your Company Name. All Rights Reserved.
</Footer>
</Layout>
);
};
export default App;
https://www.youtube.com/watch?v=7YBXKEQCfLw