본문 바로가기
국비학원/수업기록

국비 지원 개발자 과정_Day92

by 루팽 2023. 4. 11.

<시험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");
			}
		};
	}
}

댓글