<시험1-통합 구현평가자체크리스트>
Controller의 public String selectBoard() 메소드 작성
@GetMapping("bdetail.do")
public String selectBoard(@RequestParam Map<String,Object> pMap, Model model) {
logger.info("selectBoard 호출 성공");
Board board = null;
board = boardDao.selectBoard(pMap);
if(board != null) {
model.addAttribute("board", board);
return "boardDetail";
} else {
return "redirect:error.do";
}
}
Dao의 public Board selectBoard() 메소드 작성
public Board selectBoard(Map<String, Object> pMap) {
logger.info("boardList 호출 성공");
Board board = null;
try {
board = sqlSessionTemplate.selectOne("boardMapper.selectBoard", pMap);
if(board !=null) logger.info(board.getTitle());
} catch (DataAccessException e) {
logger.info("Exception : "+e.toString());
}
return board;
}
Mapper 작성
<mapper namespace="com.mybatis.mapper.BoardMapper">
<select id="selectBoard" parameterType="map" resultType="com.kh.test.board.model.vo.Board">
select bid, title, writer, content, bdate
from board
where 1=1
<if test="bid>0">
and bid = #{bid}
</if>
</select>
</mapper>
root-context.xml 작성
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>oracle.jdbc.driver.OracleDriver</value>
</property>
<property name="url">
<value>jdbc:oracle:thin:@192.168.19.3:1521:xe</value>
</property>
<property name="username">
<value>KH</value>
</property>
<property name="password">
<value>KH</value>
</property>
</bean>
<시험2-통합 구현문제해결시나리오>
통신을 마치고 DataBase Connection을 닫는 과정에서 간혹 예상치 못한 에러가 발생했다.
발생 원인
사용한 자원은 반드시 반납해야 하는데, 생성된 역순으로 close 해야 한다
기능이 정상적으로 동작할 수 있도록 코드를 수정
private JDBCTemplate() {
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static JDBCTemplate getInstance() {
if (instance == null)
instance = new JDBCTemplate();
return instance;
}
public Connection getConnection() {
String url = "jdbc:oracle:thin:@localhost:1521:xe";
String user = "BOOKMANAGER";
String password = "USER11";
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, password);
conn.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
public void commit(Connection conn) {
try {
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void rollback(Connection conn) {
try {
conn.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void close(ResultSet rset) {
try {
rset.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void close(Statement stmt) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void close(Connection conn) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 사용한 자원은 반드시 명시적으로 반납처리
// 반납시에는 생성된 역순으로 반납을 처리함
public void close(ResultSet rset, Statement stmt, Connection con) {
try {
if(rset != null) rset.close();
if(stmt != null) stmt.close();
if(con != null) con.close();
} catch (Exception e) {
e.printStackTrace();
}
}
버튼을 클릭하였으나 기능이 정상적으로 동작하지 않았다.
기능이 동작하지 않은 이유
버튼 선언보다 스크립트가 먼저이기에, defer를 붙여서 브라우저가 페이지의 파싱을 모두 끝낸 후 스크립트가 실행되도록 해야 한다.
기능이 정상적으로 동작할 수 있도록 수정한 코드
<script defer type="text/javascript">
const btnTest = document.querySelector("#btn_test")
btnTest.addEventListener('click', function(e) {
e.preventDefault()
alert('확인')
})
</script>
<button id="btn_test">확인</button>
index.js → FileInput생성 → 리턴값은 화면, ImageFileInput → name출력-클릭-파일 찾기
CardAddForm.jsx와 CardEditForm.jsx
App.jsx
/ → Login.jsx → 로그인 성공하면 CardManager.jsx이동
/manager → CardManager.jsx
고려사항 - 캡쳐링과 버블링
부모태그에서 자손태그로 전이되는 것 - 리액트 props컨벤션과 일치
index.js는 화면이 아니다
BrowserRouter
<Provider stort={store}>
<App />
데이터셋은 어디에 두는 게 좋을까? → 가능한 최상위 컴포넌트에 둔다
cards선언은? → CardManager.jsx → n건
map사용은? → CardEditor.jsx(n건을 1건씩 폼으로 넘김)
삭제하기 버튼은? → CardEditorForm.jsx → card(1건)
<index.js>
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import AuthLogic from "./service/authLogic";
import firebaseApp from "./service/firebase";
import { legacy_createStore } from "redux";
import rootReducer from "./redux/rootReducer";
import { setAuth } from "./redux/userAuth/action";
import ImageUploader from "./service/imageUploader";
import ImageFileInput from "./components/common/ImageFileInput";
const imageUploader = new ImageUploader()
// 변수선언시 소문자이면 함수, 대문자면 화면 컴포넌트
const FileInput = (props) => (
<ImageFileInput {...props} imageUploader={imageUploader} />
)
const authLogic = new AuthLogic(firebaseApp)
const store = legacy_createStore(rootReducer)
store.dispatch(setAuth(authLogic.getUserAuth(), authLogic.getGoogleAuthProvider()))
console.log(store.getState())
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<BrowserRouter>
<Provider store={store}>
<App FileInput={FileInput} />
</Provider>
</BrowserRouter>
</>
);
<App.jsx>
import { Route, Routes } from "react-router-dom";
import "./App.css";
import styled from "styled-components";
import Login from "./components/login/Login";
import CardManager from "./components/page/CardManager";
const AppDiv = styled.div`
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-item: center;
background-color: #FFF3E2;
`
const App = ({FileInput}) => {
return (
<>
<AppDiv>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/manager" element={<CardManager FileInput={FileInput} />} />
</Routes>
</AppDiv>
</>
);
}
export default App;
<CardManager.jsx>
import React, { useState } from 'react'
import Header from '../include/Header'
import Footer from '../include/Footer'
import styled from 'styled-components'
import Preview from './Preview'
import CardEditor from './CardEditor'
import { useSelector } from 'react-redux'
const MakerDiv = styled.div`
width: 100%;
height: 100%;
max-width: 80rem;
display: flex;
flex-direction: column;
background-color: white;
`
const ContainerDiv = styled.div`
display: flex;
flex: 1;
min-height: 0;
`
const CardManager = ({FileInput}) => {
// auth객체 정보 수집위해 -> userAuth.auth
const {userAuth} = useSelector(store => store)
const [cards, setCards] = useState({
'1':{
id: '1',
name: '이성계',
company: 'Samsung',
theme: 'dark',
title: 'Software Engineer',
email: 'lee@gmail.com',
message: 'go for it',
fileName: 'lee',
fileURL: 'https://res.cloudinary.com/drxzbple6/image/upload/v1681195116/bdehqu3hpq32zpt3nq4h.png',
},
'2':{
id: '2',
name: '김유신',
company: 'Cupang',
theme: 'light',
title: 'Software Engineer',
email: 'kim@gmail.com',
message: 'I can do it',
fileName: 'kim',
fileURL: 'https://res.cloudinary.com/drxzbple6/image/upload/v1679541307/qt4tndsmuc9nm9aaqjie.png',
},
'3':{
id: '3',
name: '유재석',
company: 'MBC',
theme: 'colorful',
title: 'Software Engineer',
email: 'you@gmail.com',
message: 'we are the world',
fileName: 'you',
fileURL: 'https://res.cloudinary.com/drxzbple6/image/upload/v1681195261/hlurpthh7guz7upmrh6q.png',
}
});
// 데이터셋은 CardManager에 있음 -> 원본은 건들지 않고 복사본 사용 -> 삭제, 추가, 수정
// 삭제 버튼은 CardEditorForm에 있음 - 삭제 대상도 거기 있음
// 자바스크립트는 파라미터 사용가능
// 파라미터 값은 언제 결정? -> 사용자가 삭제버튼을 클릭했을때 - deleteCard함수 호출
// 그때 파라미터로 card를 전달받을 수 있음
const deleteCard = card => { // 삭제하고자하는 카드정보를 여기서 결정할 수 없음 -> CardEditorForm으로 넘겨줌
console.log(card)
setCards(cards => { // 리렌더링 즉시 -> return -> 내 안에 컴포넌트 -> rendering
// 스프링 부트에서 넘어오는 데이터셋은 useState매핑 -> 화면이 다시 그려진다
const updated = {...cards} // 복사 spread 연산자 -> 깊은복사
delete updated[card.id]
return updated // 복사본이 리턴된다
})
}
return (
<MakerDiv>
<Header />
<ContainerDiv>
<CardEditor FileInput={FileInput} cards={cards} deleteCard={deleteCard} />
<Preview cards={cards} />
</ContainerDiv>
<Footer />
</MakerDiv>
)
}
export default CardManager
<CardEditor.jsx>
import React from 'react'
import CardAddForm from './CardAddForm'
import styled from 'styled-components'
import CardEditorForm from './CardEditorForm'
const EditorDiv = styled.div`
flex-basis: 50%;
border-right: 1px solid #9E7676;/* editor와 preview사이에 구분선 넣기 */
padding: 0.5em 2em;
overflow-y: auto;
`
const TitleH1 = styled.h1`
width: 100%;
text-align: center;
margin-bottom: 1em;
color: #594545;
`
const CardEditor = ({ FileInput, cards, deleteCard}) => {
console.log(cards); // 3건 출력 - CardManager.jsx에 선언된 cards, setCards
return (
<EditorDiv>
<TitleH1>Card Editor</TitleH1>
{Object.keys(cards).map(key => (
/* cards 3개 로우에 대해서 한 개 card정보만 전달해야함 */
<CardEditorForm key={key} FileInput={FileInput} card={cards[key]} deleteCard={deleteCard} />
))
}
<CardAddForm FileInput={FileInput} />
</EditorDiv>
)
}
export default CardEditor
<CardEditorForm.jsx>
import React from 'react'
import styled from 'styled-components';
import Button from '../common/Button';
import ImageFileInput from '../common/ImageFileInput';
const Form = styled.form`
display: flex;
width: 100%;
flex-wrap: wrap; /* 한 줄에 하나씩 떨어질 수 있도록 랩을 주고 */
border-top: 1px solid black;
border-left: 1px solid black;
margin-bottom: 1em;
`
const NameInput = styled.input`
font-size: 0.8rem;
width: 100%;
border: 0;
padding: 0.5em;
border-bottom: 1px solid black;
border-right: 1px solid black;
background: #F5EBE0;//#F5EBE0, #FEFCF3
flex: 1 1 30%; /* 30%주어서 한 줄에 3개씩 나오게 하고 */
`
const CompanyInput = styled.input`
font-size: 0.8rem;
width: 100%;
border: 0;
padding: 0.5em;
border-bottom: 1px solid black;
border-right: 1px solid black;
background: #F5EBE0;
flex: 1 1 30%; /* 30%주어서 한 줄에 3개씩 나오게 하고 */
`
const TitleInput = styled.input`
font-size: 0.8rem;
width: 100%;
border: 0;
padding: 0.5em;
border-bottom: 1px solid black;
border-right: 1px solid black;
background: #F5EBE0;
flex: 1 1 30%; /* 30%주어서 한 줄에 3개씩 나오게 하고 */
`
const EmailInput = styled.input`
font-size: 0.8rem;
width: 100%;
border: 0;
padding: 0.5em;
border-bottom: 1px solid black;
border-right: 1px solid black;
background: #F5EBE0;
flex: 1 1 30%; /* 30%주어서 한 줄에 3개씩 나오게 하고 */
`
const ThemeSelect = styled.select`
font-size: 0.8rem;
width: 100%;
border: 0;
padding: 0.5em;
border-bottom: 1px solid black;
border-right: 1px solid black;
background: #F5EBE0;
flex: 1 1 30%; /* 30%주어서 한 줄에 3개씩 나오게 하고 */
`
const MessageTextArea = styled.textarea`
font-size: 0.8rem;
width: 100%;
border: 0;
padding: 0.5em;
border-bottom: 1px solid black;
border-right: 1px solid black;
background: #F5EBE0;
`
const FileInputDiv = styled.div`
font-size: 0.8rem;
width: 100%;
border: 0;
padding: 0.5em;
border-bottom: 1px solid black;
border-right: 1px solid black;
background: #F5EBE0;
`
const CardEditorForm = ({ FileInput, card, deleteCard}) => {
const {name, company, title, email, message, theme, fileName, fileURL} = card;
const onFileChange = (file) => {
console.log(file);
// useState에 초기화된 배열에 사용자가 선택한 fileName과 fileURL을 추가해서 배열을 수정해준다
}
const onChange = (event) => {
if(event.currentTarget == null) {
return;
}
//브라우저에서 기본적인 이벤트 처리를 하지 않도록 처리한다
event.preventDefault();
/* updateCard( {
...card,
[event.currentTarget.name]: event.currentTarget.value,
}); */
};
//maker.jsx에서 deleteCard호출할때 실제 기능 처리할 코드임- 이걸 해야 삭제됨
const onSubmit = () => {
// 여기서 호출되는 함수는 CardManager에서 옴
// 파라미터엔 삭제할 card
deleteCard(card)
};
return (
<Form>
<NameInput
type="text" name="name"
value={name}
onChange={onChange}
/>
<CompanyInput
type="text"
name="company"
value={company}
onChange={onChange}
/>
<ThemeSelect
name="theme"
value={theme}
onChange={onChange}
>
<option value="light">light</option>
<option value="dark">dark</option>
<option value="colorful">colorful</option>
</ThemeSelect>
<TitleInput
type="text" name="title"
value={title}
onChange={onChange}
/>
<EmailInput
type="text"
name="email"
value={email}
onChange={onChange}
/>
<MessageTextArea
name="message"
value={message}
onChange={onChange}
>
</MessageTextArea>
<FileInputDiv>
<FileInput name={fileName} onFileChange={onFileChange} />
</FileInputDiv>
<Button name="Delete" onClick={onSubmit}/>
</Form>
)
}
export default CardEditorForm
<Card.jsx>
import React from 'react'
import styled from 'styled-components'
import styles from './card.module.css'
const CardLi = styled.li`
display: flex; /* 이름같은것들 이미지 옆으로 보내기 */
align-items: center; /* flex속성일때 사용 가능 */
width: 100%;
background-color: #FFE5CA;
margin-bottom: 0.5em;
border-radius: 1em;
padding: 0.2em 0;
box-shadow: 6px 6px 8px 0px rgba(217,217,217,1);
max-width: 30em; /* 넓이 제약 */
`
const AvatarImg = styled.img`
width: 10em;
height: 10em;
padding: 1em;
border-radius:20%
margin-right: 1em; /* 이미지와 글자사이에 마진 */
margin-left: 0.5em; /* 이미지 앞쪽에 마진 */
`
const NameH1 = styled.h1`
margin: 0;
font-size: 1.2rem;
margin-bottom: 0.2em;
`
const CompanyP = styled.p`
margin: 0;
font-size: 0.8rem;
margin-bottom: 1em;
&::after {
content: "";
display: block;
width: 90%;
height: 2px;
transform: translateY(0.5em); /* 회사와 직책 사이에 선그림 */
background-color: #FFE5CA;
}
`
const TitleP = styled.p`
margin: 0;
font-size: 0.8rem;
margin-bottom: 0.2em;
`
const EmailP = styled.p`
margin: 0;
font-size: 0.8rem;
margin-bottom: 0.2em;
`
const MessageP = styled.p`
margin: 0;
font-size: 0.8rem;
margin-bottom: 0.2em;
`
const Card = ({card}) => {
const DEFAULT_IMG = '/images/icon-coin.png'
const {name, company, title, email, message, theme, fileName, fileURL } = card
const getStyles = (theme) => {
switch(theme) {
case 'dark':
return styles.dark;
case 'light':
return styles.light;
case 'colorful':
return styles.colorful;
default:
throw new Error(`unknown theme:${theme}`)
}
}
const url = fileURL || DEFAULT_IMG
return (
<CardLi className={`${styles.card} ${getStyles(theme)}`}>
<AvatarImg src={url} alt='profile image' />
<div className={{width: '100%'}}>
<NameH1>{name}</NameH1>
<CompanyP>{company}</CompanyP>
<TitleP>{title}</TitleP>
<EmailP>{email}</EmailP>
<MessageP>{message}</MessageP>
</div>
</CardLi>
)
}
export default Card
<ImageFileInput.jsx>
import React, { useRef, useState } from 'react'
import styled from 'styled-components'
import styles from './imageFileInput.module.css';
const ContainerDiv = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center; /* 아이템을 가운데 오게함 */
align-items: center; /* 아이템을 중간 middle에 오게함 */
`
const HiddenInput = styled.input`
display: none;
`
const ImageFileInput = ({ imageUploader, name, onFileChange}) => {
console.log(name);
const [loading, setLoading] = useState(false);
const inputRef = useRef();
const onButtonClick = (event) => {
event.preventDefault();
// No file이 클릭되었을 때 버튼 클릭하기
inputRef.current.click();
}
// 파일이 변경될 때
// 이제 이 파일을 우리 업로드 서비스에 전달하면 되겠죠 자 그래서 이제 imageUploader에
// 있는 upload라는 함수를 이용해서 우리가 선택된 이 파일을 전달해 줄거고요
// 처음 우리가 정의 할 때 async 였죠 그래서 promise가 리턴이 됩니다
// 그래서 then하고 catch를 하고 하면 되고요 이렇게 그냥 promise를 이용해서 then then then
// 하셔도 되고요 아니면 이 리스너 자체를 이 콜백 자체를 async라고 붙일 수가 있어요
// 이렇게 해서 uploaded는 await 이것이 실행 될 때 까지 기다렸다가 완료가 되면 여기
// uploaded는 await 이것이 실행될 때까지 기다렸다가 완료가 되면 여기 uploaded에
// 할당이 되는 거죠 이제 완료가 되면 onFileChange라는 여기 prop으로 전달된
// 이 콜백함수에 우리 파일이 바뀌었어 라고 알려 줘야 되죠
const onChange = async event => {
// 아래에서 로딩중 정보를 true로 변경해 주고 아래 34번에서 로딩이 끝나고 나면 다시 false로 변경해줌
setLoading(true);
const uploaded = await imageUploader.upload(event.target.files[0]);
console.log(uploaded);
setLoading(false);
onFileChange({
//name: 'fileName',
name: uploaded.original_filename,
//url: 'url'
url: uploaded.url,
});
};
return (
<ContainerDiv>
<HiddenInput
ref={inputRef}
type="file" accept="image/*" name="file"
onChange={onChange}
/>
{/* 로딩중이 아니면 아래 코드가 처리 */}
{ !loading && (
<button className={`${styles.button} ${name ? styles.pink : styles.grey}`} onClick={onButtonClick}>
{name || 'No file'}
</button>
)}
{/* 로딩 중이면 아래 코드 처리 */}
{ loading && <div className={styles.loading}></div> }
</ContainerDiv>
)
}
export default ImageFileInput
<ImageUploader.js>
class ImageUploader {
// 여기에는 상태는 없어요
async upload(file) {
//upload했다면 그 URL전달
//아래 수동으로 했던 거 지우고
//return 'file';
const data = new FormData(); // 외부 클라우드 시스템과 연동시에 꼭 필요한 객체
// input type file에서 선택한 파일에대한 정보 담기
data.append("file", file);
// Cloudinary에서 제공하는 서비스를 이용하여 사용자가 선택한 파일에대한 URL정보만
// 제공받아서 처리하므로 upload_preset은 unsigned로 받을 것
data.append("upload_preset", "키"); // unsigned - 인증과정 없이 사용할 때
/*
POST를 이용하니까 POST에 추가하는 데이터 입력하고 fetch를 이용해서 여기 우리가 URL 만들고
POST한 거 데이터를 전송한 다음에 완료가 되면 이제 result를 받아서 result에 있는 것을 json으로
변환해서 리턴해 줄거예요
*/
const result = await fetch(
"https://api.cloudinary.com/v1_1/키/upload",
{
method: "POST",
body: data,
}
);
return await result.json();
}
}
export default ImageUploader;
//https://cloudinary.com/documentation/upload_images
<server.js>
// node_modules 에 있는 express 관련 파일을 가져온다.
const express = require("express");
const path = require("path");
const cors = require("cors");
// express 는 함수이므로, 반환값을 변수에 저장한다.
const app = express();
app.use(express.json());
// 8000 포트로 서버 오픈
app.use(cors());
app.use(express.static("build"));
//app.use('/favicon.png', express.static(path.join(__dirname + '/build/favicon.png')));
const port = 7000;
app.get("*", function (req, res) {
res.sendFile(path.join(__dirname + "/build", "index.html"));
});
app.listen(port, () => {
console.log(path.join(__dirname + "/build", "index.html"));
});
// 이제 터미널에 node app.js 를 입력해보자.
<Mblog1Application.java>
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@SpringBootApplication
public class Mblog1Application {
public static void main(String[] args) {
SpringApplication.run(Mblog1Application.class, args);
}
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 두 포트번호 모두 서비스를 제공하지만 3000번은 동기화된 서비스 제공, 7000번은 build된 코드까지만 제공된다
// 결과적으로 3000번은 개발 서버로, 7000번은 서비스 서버로 하용하면 될 것이다
registry.addMapping("/**").allowedOrigins("http://localhost:3000", "http://localhost:7000");
}
};
}
}
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day94 (0) | 2023.04.13 |
---|---|
국비 지원 개발자 과정_Day93 (0) | 2023.04.12 |
국비 지원 개발자 과정_Day91 (0) | 2023.04.07 |
국비 지원 개발자 과정_Day90 (0) | 2023.04.06 |
국비 지원 개발자 과정_Day89 (0) | 2023.04.05 |
댓글