Github : https://github.com/HAN-SEOHYUN/Happy-Christmas
※ BackEnd
▶ data-source.ts
require("dotenv").config();
export const AppDataSource = new DataSource({
~ other code ~
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT),
username: process.env.MYSQL_USERNAME,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
});
DataSource 객체를 생성해 DB 연결을 관리하기 위한 환경변수들을 입력해줌.
먼저 [.env] 파일을 생성해 환경변수를 입력하고,
dotenv 모듈을 설치해서 process.env 객체에 저장된 환경변수들을 불러옴
▷ .env 란 ?
- Node.js 에서 사용하는 환경변수를 저장하기 위한 파일.
- git 과 같은 VCS 에 관리되지 않는다.
- 일반적으로 .env 파일은 어플리케이션을 실행할 때 읽어지고, 읽어진 환경변수들은 process.env 객체에 저장된다.
- 이 객체는 Node.js 어플리케이션 전역에서 접근이 가능하다.
▷ dotenv 는 무엇인가 ?
- .env 파일을 읽어서 환경변수를 설정해주는 Node.js 모듈
- dotenv 를 통해 .env 환경변수를 process.env 객체에 저장할 수 있다.
▷ DataSource 객체 ?
- DB 연결을 관리하기 위한 객체
▶ index.ts
AppDataSource.initialize()
.then(async () => {
// create express app
const app = express();
//cors
app.use(cors({
origin:'http://localhost:3000'
}));
//body-parser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
//routes
app.use("/api/to", toRouter);
app.use("/api/from", fromRouter);
// run app
app.listen(PORT);
console.log("Server running on port: " + PORT);
})
.catch((error) => console.log(error));
initialize 메소드가 실행되고, 초기화 작업이 완료되면 콜백함수가 실행됨.
▷ 콜백함수 실행단계
- express 앱 생성
- cors 설정
- body-parser
- 라우팅
▷ initialize() 란 ?
- initialize() 메소드는 앱을 시작할 때 필요한 초기화 작업을 수행하는 메소드
- 실행할때마다 데이터베이스 스키마가 동기화됨
▷ body parser 사용이유 ?
- Express.js에서 body-parser 미들웨어를 사용하는 이유는 HTTP request 에서 데이터를 추출하기 위함
▶ Entity-FromSian
@Entity()
export class FromSian {
@PrimaryGeneratedColumn()
id: number;
@Column()
recipient: string;
@Column()
pwd : string;
@Column()
message : string;
@Column({default : ()=>'CURRENT_TIMESTAMP'})
createdAt : Date;
}
@Entity 데코레이터를 사용해서 내가 작성한 메세지 정보를 관리하는 FromSian 테이블 생성
▶ Entity-ToSian
@Entity()
export class ToSian {
@PrimaryGeneratedColumn()
id: number;
@Column()
sender: string;
@Column()
message : string;
@Column({default : ()=>'CURRENT_TIMESTAMP'})
createdAt : Date;
}
나에게 작성된 메세지 정보를 관리하는 ToSian 테이블
▶ 라우터-from.routes.ts
const router = express.Router();
router.get('/recipient/:recipient',fromController.getCountByName); //수정 전
router.get('/recipient',fromController.getCountByName);//수정 후
router.get('/:id', fromController.getMessageById);
router.post('/',fromController.confirmPassword);
export default router;
피드백 : path parameter => query string 으로 변경하면 역할을 명확하게 구분할 수 있을 것 같음
▶ 라우터-to.routes.ts
const router = express.Router();
router.post('/',toController.saveMesage);
export default router;
데이터가 향하는 테이블 별로 라우터 파일을 분리.
▶ 컨트롤러-FromController
const fromSianRepository = AppDataSource.getRepository(FromSian);
FromSian 엔티티에 관련된 작업을 처리하는 Repository 를 가져옴
//이름을 넣으면 이름에 해당하는 row 의 count를 알려줌
export async function getCountByName(req:Request, res:Response){
let inputRecipient = req.params.recipient;
await fromSianRepository
.findAndCount({where : {recipient: inputRecipient}})
.then((object)=> {
res.send({
success: true,
data : {
count: object[1]
}
})
})
.catch((err)=>console.log(err));
}
.
.
.
피드백
1. async await 를 사용하면 then() 을 사용할 필요가 없음 !
2. path parameter => query string 변경
3. 변수명 object 적절하지 않음
4. if 문 사용할때는 != 사항을 먼저 기재하면 가독성이 높아짐
피드백 적용 후
const fromSianRepository = AppDataSource.getRepository(FromSian);
export async function getCountByName(req:Request, res:Response){
const inputRecipient = req.query.recipient as string;
try{
const [fromSian_object,count] = await fromSianRepository
.findAndCount({where : {recipient: inputRecipient}});
res.send({
success: true,
data : {
fromSian_object,
count
}
})
}catch(err){
console.log(err);
}
}
export async function getMessageById(req:Request, res:Response){
const inputId = parseInt(req.params.id);
try{
const fromSian_object = await fromSianRepository
.findOneBy({
id: inputId,
});
res.send({
data : {
id : fromSian_object?.id,
recipient : fromSian_object?.recipient,
createdAt : fromSian_object?.createdAt,
message : fromSian_object?.message,
}
})
}catch(err){
console.log(err);
}
}
export async function confirmPassword(req:Request, res:Response){
const inputRecipient = req.body.recipient;
const inputPwd = req.body.pwd;
try{
const fromSian_object = await fromSianRepository
.findOneBy({
recipient: inputRecipient
});
if(fromSian_object?.pwd !== inputPwd){
res.send({
success:false
});
}
res.send({
success: true,
data : {id : fromSian_object?.id}
});
}catch(err){
console.log(err);
}
}
- getCountByName : 요청으로 받은 이름에 해당하는 데이터가 있는지 확인을 하고, 데이터가 존재할 경우에 응답을 보내줌
- getMessageById : 요청으로 받은 아이디에 해당하는 메세지 객체를 응답하는데, pwd 는 보내주지 않음
- confirmPassword : 요청 받은 name 과 pwd 가 일치할 경우 success 여부와 data 를 응답하고, 그렇지 않을 경우 false 를 응답
▶ 컨트롤러-ToController
export async function saveMesage(req:Request, res:Response){
try{
const toSian_object = await toSianRepository.save(req.body);
res.send({
status : "success",
data : toSian_object
})
}catch(err){
console.log(err);
}
}
saveMessage : 레포지토리의 save() 메서드를 사용해서 요청된 값을 저장해주고 (메세지 작성) status와 data 를 리턴
▷ Repository 란 ?
- 각 엔티티는 엔티티관련 작업을 처리하는 Repository 를 가지고 있다.
※ FrontEnd
▶ routes.tsx
const routes: Route[] = [
{
path: "/",
component: HomePage,
},
{
path: "/detail",
component: DetailMessage,
},
{
path: "/register",
component: CreateMessage,
},
{
path: "*",
component: NotFoundPage,
},
];
export default routes;
Route 배열에 경로와 해당 컴포넌트를 넣어줌
위의 경로중 어떤 것도 해당하지 않을 때 404 페이지로 이동됨
▶ App.tsx
const App: React.FC = () => {
return (
<Router>
<NavBar />
<div className="container mt-3">
<Switch>
{routes.map((route,index) => {
return (
<Route
key={route.path}
exact
path={route.path}
component={route.component}
/>
);
})}
</Switch>
</div>
</Router>
);
};
App.tsx 에서 map 함수의 인자로 import 된 routes 배열을 넣고 routes 값을 Route 컴포넌트 속성의 값으로 각각 할당
=> 여러개의 Route 컴포넌트를 리턴하는 구조
▷ map 함수 ?
인자로 주어진 요소에 어떤 함수를 적용한 결과를 새로운 배열로 만들어 리턴한다.
▷ Route 컴포넌트 ?
Route 컴포넌트는 주소가 지정된 경로와 일치할 때 컴포넌트를 렌더링한다.
▷ Switch 컴포넌트 ?
Route 컴포넌트 중 첫번째로 매칭되는 path 를 가진 컴포넌트를 렌더링시킨다.
주소가 일치하는 Route 컴포넌트가 찾아지면 그 이후의 Route 컴포넌트는 처리되지 않는다.
▷ Switch 를 사용하지 않는다면 ?
컴포넌트가 중복되어 렌더링 될 수 있다.
▶ NavBar.tsx
const NavBar = () => {
return (
~ other codes ~
<Link
className="btn btn-success me-2"
type="button"
id="from-Btn"
onClick={() => {
if (window.location.pathname === "/") {
window.location.reload();
}
}}
to="/"
>
~ other codes ~
);
};
export default NavBar;
HomePage에서 From.Sian 버튼을 누르면 새로고침이 되도록.
NavBar 컴포넌트는 페이지가 이동해도 상단에 고정되어야하기 때문에 App.tsx 에서 import
▶ HomePage.tsx
const HomePage: React.FC = () => {
const [recipient, setRecipient] = useState<string>("");
const [count, setCount] = useState<number>();
const handleMessageCount = async () => {//
try {
const res = await getCountByName(recipient);
setCount(res.count);
if (!res.count) {
alert(recipient + "에게 등록된 메세지가 없습니다");
}
} catch (err) {
alert(err);
}
};
피드백
string 연결 시 '+' 연산자 사용 지양
if (!res.count) {
alert(`${recipient}에게 등록된 메세지가 없습니다`);
}
return (
<div>
<div className="d-flex justify-content-center mb-3">
<img
className="w-50 "
src="img/merry-christmas-word.jpg"
alt="merry-christmas-word"
></img>
</div>
<div className="my-4">
<p className="font-size-md">
<b>
이름을 입력하면 등록된 편지가 있는지 확인할 수 있습니다.
<br />
방문해주신 모든분들 남은 한 해 즐겁고, 행복하게 마무리 잘 하시고
<br /> 즐거운 크리스마스 보내시길 바랍니다.
</b>
</p>
</div>
<div>
<div className="input-group mb-3">
<input
type="text"
name="recipient"
className="form-control"
placeholder="이름을 입력하세요 ex) 홍길동"
aria-label="name"
aria-describedby="button-addon2"
onChange={(event) => {
setRecipient(event.target.value);
//alert 카드 이름이 수정되는 오류 막아주기
setCount(0); //등록된 이름 없음(0) 으로 처리해서 value 수정 시 alert 카드가 안보이도록
}}
/>
<button
className="btn btn-outline-secondary"
name='check-Btn'
type="button"
disabled={!recipient}
onClick={handleMessageCount}
>
확인하기
</button>
</div>
{count ? <AlertCard recipient={recipient} />: null}
</div>
</div>
);
};
export default HomePage;
이름을 입력하고 확인하기 버튼을 누르면 getCount() 메서드 실행됨
getCount() 메서드는 입력된 이름을 서버에 보내서 해당이름으로 등록된 메세지가 있는지 확인해줌
응답받은 count 데이터를 useState 에 넣고, 만약 응답받은 데이터에 count 가 없을 경우 Alert
count 가 있을 경우 AlertCard 컴포넌트를 보여주고, 이 컴포넌트에 props 로 입력된 이름(recipient)을 전달
▷ React.FC 란 ?
- 함수형 컴포넌트를 정의할 때 사용하는 타입
피드백
삼항연산자보다 && 연산자 사용
▶ AlertCard.tsx
const AlertCard: React.FC<Props> = ({ recipient }) => {
const history = useHistory();
const [pwd, setPwd] = useState<string>("");
let isConfirmed = false;
const handleMoveDetail = (id: number, isConfirmed: boolean) => {
history.push({
pathname: "/detail",
state: {
id: id,
isConfirmed: isConfirmed,
},
});
};
//비밀번호가 일치하는지 확인해줌
const handleCheckPwd = async () => {
try {
const res = await checkPwd(recipient, pwd);
if(!res){
alert("비밀번호가 올바르지 않습니다 !");
}else{
isConfirmed = true;
handleMoveDetail(res.id, isConfirmed);
}
} catch (err) {
alert(err);
history.push(`/`);
}
};
return (
<div className="alert-card">
<hr />
<div className="card text-center">
<div className="card-body">
<h5 className="card-title">
<FontAwesomeIcon icon={faEnvelope} />
{recipient} 님 에게
</h5>
<p className="card-text">전달된 크리스마스 메세지가 1개 있습니다</p>
<div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">
비밀번호
</span>
<input
type="text"
name ="pwd-input"
className="form-control"
placeholder="비밀번호입력"
onChange={(event) => {
setPwd(event.target.value);
}}
/>
</div>
<button
className="btn btn-primary"
name="checkPwd-Btn"
onClick={handleCheckPwd}>
메세지 읽기
</button>
</div>
</div>
</div>
);
};
export default AlertCard;
AlertCard 컴포넌트에서 Props(recipient) 를 전달 받음
비밀번호를 입력하고 확인 버튼을 누르면 checkPwdHandler 메서드 실행
checkPwdHandler :입력된 비밀번호를 서버에 요청해서 일치 여부를 응답받고 비밀번호가 일치할 경우 moveDetail 메서드 실행, 일치하지 않을 경우에는 Alert 발생
moveDetail : 상세페이지로 이동을 시켜주고, state 로 id 와 비밀번호를 일치하는지에 대한 정보를 담은 isConfirmed 를 전달
▶ DetailMessage.tsx
const DetailMessage: React.FC = () => {
const location = useLocation();
const history = useHistory();
const [message_object, setMessage_object] = useState<Message | null>(null);
const [loading, setLoading] = useState(true);
const state = location.state as PropsType;
//메세지 객체를 가져와서 페이지에 정보를 뿌려줌
const handleShowMessage = async (id: number, isConfirmed: boolean) => {
if (!isConfirmed) {
alert("올바른 접근이 아닙니다 !");
history.push(`/`);
}
try {
const message = await getMessageById(id);
setMessage_object(message);
} catch (err) {
alert(err);
}
setLoading(false);
};
useEffect(() => {
handleShowMessage(state.id, state.isConfirmed);
console.log(location);
}, [state.id]); //eslint-disable-line
return (
<>
{loading ? (
<LoadingSpinner />
) : (
<>
<div>
<div className="d-flex">
<h1 className="flex-grow-1">{message_object?.recipient || "없음"}</h1>
</div>
<small className="text-muted">{message_object?.createdAt}</small>
<hr />
<p>{message_object?.message}</p>
</div>
<Link id="write-Btn" className="btn btn-primary" to={`/register`}>
편지쓰기
</Link>
</>
)}
</>
);
};
export default DetailMessage;
moveDetail 에서 state 로 보낸 props 들을 state 에 담고,
useEffect를 통해서 컴포넌트가 마운트 될 때 getMessage 메서드가 실행되도록.
getMessage 메서드는 서버로부터 id 에 해당하는 메세지 객체를 받아오고
만약 응답받은 메세지 객체가 있다면, Object useState 에 할당되어 화면에 정보가 뿌려지고, loading useState 가 false 로 변경됨
따라서 서버로부터 응답을 받기 전까지 사용자는 loadingSpinner 라는 컴포넌트를 보게됨
▷ 마운트 ?
리액트에서는 컴포넌트가 처음 렌더링될 때 '마운트' 된다고 함
▷ Link ?
새로고침하지 않고 서로 다른 라우트 사이를 이동하는 것을 돕는 컴포넌트
▶ CreateMessage.tsx
const CreateMessage: React.FC = () => {
const history = useHistory();
const [sender, setSender] = useState<string>("");
const [message, setMessage] = useState<string>("");
const handleMessageSubmit = () => {
try {
postToSian(sender, message);
alert(`${sender}님의 메세지가 등록되었습니다. HappyChristmas!`);
history.push(`/`);
} catch (err) {
alert(err + "에러");
history.push(`/`);
}
};
return (
<>
<h1 className="">
Write a Christmas Letter to <span className="font-size-md">Sian</span>
<FontAwesomeIcon icon={faGift} />
</h1>
<hr />
<div className="mb-3">
<label className="form-label">작성자</label>
<input
className="form-control"
name="sender"
onChange={(event) => {
setSender(event.target.value);
}}
/>
</div>
<div className="mb-3">
<label className="form-label">Christmas Message</label>
<textarea
className="form-control"
rows={10}
name="message"
onChange={(event) => {
setMessage(event.target.value);
}}
/>
</div>
<button
className="btn btn-primary"
onClick={handleMessageSubmit}
name="create-Btn"
disabled={!sender || !message}
>
작성하기
</button>
<Link
className="btn btn-danger ms-2"
to="/"
id="home-Btn"
>
돌아가기
</Link>
</>
);
};
export default CreateMessage;
등록 페이지에서는 작성자와 내용을 입력하고 작성하기 버튼을 클릭하면 onSubmit 메서드가 실행됨
onSubmit 메서드는 입력된 작성자와 내용을 등록시켜주는데, 등록이 완료되면 Alert 이 뜨고 메인페이지로 이동됨
작성자와 내용 둘중에 하나라도 입력이 되지않으면 버튼은 비활성화
'프로젝트' 카테고리의 다른 글
[프로젝트] Mixed Content 오류로 인한 API 요청 차단 – Nginx를 통한 HTTPS 리버스 프록시로 해결 (0) | 2025.07.18 |
---|---|
[프로젝트] 반려동물 자랑 토이프로젝트 소개 (3) | 2025.06.09 |
[React & Typescript 프로젝트] Happy-Christmas ! (0) | 2022.12.30 |
[Decoupled Architecture] CORS (0) | 2022.12.07 |
[Decoupled Architecture] 프론트엔드 & 백엔드 서버 분리 환경에서의 데이터 전달 (0) | 2022.12.06 |