
Springboot REMOTE Oracle connect 오류 해결
datasource: mysql: # driver-class-name: com.mysql.cj.jdbc.Driver # jdbc-url: jdbc:log4jdbc:mysql://"IP":3306/bilab?&characterEncoding=UTF-8&serverTimezone=Asia/Seoul driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy jdbc-url: jdbc:log4jdbc:mysql://"IP":3306/bilab?&characterEncoding=UTF-8&serverTimezone=Asia/Seoul username: kang password: kang1234 oracle: # driver-class-name: oracle.jdbc.driver.OracleDriver # jdbc-url: jdbc:oracle:thin:@localhost:1521/xe driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy # jdbc-url: jdbc:log4jdbc:oracle:thin:@localhost:1521/xe jdbc-url: jdbc:log4jdbc:oracle:thin:"IP":1521:xe # jdbc-url: jdbc:oracle:thin:@"IP":1521:xe username: kang password: 1234 # hikari: # jdbc-url: IO 오류: Connection refused (CONNECTION_ID= 뭐시기가 나오면, 아래 의존성을 추가. <!-- 아래 의존성이 없으면 원격 ORACLE 접속이 안 됨 --> <dependency> <groupId>com.oracle.ojdbc</groupId> <artifactId>orai18n</artifactId> <version>19.3.0.0</version> </dependency> <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>net.lunalabs</groupId> <artifactId>central</artifactId> <version>0.0.1-SNAPSHOT</version> <name>central</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <!-- log4jdbc --> <dependency> <groupId>org.bgee.log4jdbc-log4j2</groupId> <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId> <version>1.16</version> </dependency> <!-- 아래 의존성이 없으면 원격 ORACLE 접속이 안 됨 --> <dependency> <groupId>com.oracle.ojdbc</groupId> <artifactId>orai18n</artifactId> <version>19.3.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MariaDB는 다른 식으로 연결.. --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 스프링 부트에서 Log4j2를 사용하기위해선 내부로깅에서 쓰이는 의존성을 제외해주어야 합니다. 기본적으로 Spring은 Slf4j라는 로깅 프레임워크를 사용합니다. 구현체를 손쉽게 교체할 수 있도록 도와주는 프레임 워크입니다. Slf4j는 인터페이스고 내부 구현체로 logback을 가지고 있는데, Log4j2를 사용하기 위해 exclude 해야 합니다. --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.oracle.database.jdbc</groupId> <artifactId>ojdbc8</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mariadb.jdbc</groupId> <artifactId>mariadb-java-client</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <finalName>bilabCs</finalName> <plugins> <plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>1.5.8</version> <executions> <execution> <id>generate-docs</id> <phase>prepare-package</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>html</backend> <doctype>book</doctype> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-asciidoctor</artifactId> <version>${spring-restdocs.version}</version> </dependency> </dependencies> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>

자바스크립트 기초 5
JS 객체 소환법 자바스크립트에서 기본타입을 제외한 모든 값은 "객체" 여기서 객체란, ‘이름(key): 값(value)’형태의 프로퍼티를 저장하는 일종의 컨테이너라 보면 됨. 기본타입 객체는 하나의 값만을 갖지만, 참조타입 객체는 여러 개의 값을 가질 수 있슴. 객체의 프로퍼티로 함수가 포함할 수 있으며, 이러한 함수 프로퍼티를 "메서드"라고 함. Object() 생성자 함수 var me = new Object(); me.name = ‘NY KIM’; me.gender = ‘female’; me.age = 25; console.log(me); //결과: { name: ‘NY KIM’, gender: ‘female’, age: 25 } 객체 리터럴 방식 var me = { name : ‘NY KIM’, gender: ‘female’, age: 25 }; 객체에 접근방법 me.name me[‘name’] //프로퍼티가 표현식이거나 예약어인 경우 대괄호 접근법을 써야 합니다! me.full-name //불가. 출력값: NaN me['full-name'] //가능 full(undefined)에서 name(undefiend)을 빼는 연산을 했기 때문이죠. undefined는 존재하지 않는 프로퍼티값에 접근할 때 출력되며, NaN은 수치 연산을 해서 정상적인 값을 얻지 못할 때 출력됩니다. 객체에 접근하여 값을 할당했을 때, 해당 프로퍼티가 있으면 → 값을 갱신하고, 해당 프로퍼티가 없으면 → 값을 할당한다 https://nykim.work/33 undefined 와 null undefined 는 데이터 타입이자 값이다. 아무 것도 존재하지 않는 다는 표현의 값으로 생각하면 좋을 듯하다. 모든 변수에 값이 할당 되지 않는 경우 이 undefined는 값으로써 값이 할당되지 않은 모든 변수들이 자바스크립트 Runtime 시 할당되어 출력되게 된다. undefined는 ‘없음'을 뜻하는 데이터 타입이다. 근데 자바스크립트에도 다른 정적 언어와 마찬가지로 null 이라는 데이터 타입이 존재한다. const n = null; console.log(typeof n); null 도 데이터 타입이기 때문에 null 이라는 출력값을 기대할 수 있겠지만 전혀 예상과 다른 값이 나온다. 이건 명백한 자바스크립트의 오류이다. 그래서 둘의 차이는? undefined은 개발자가 의도적으로 값을 할당할 필요가 없다. 자바스크립트가 코드를 실행할 때 변수에 적절한 값이 할당 되어있지 않거나 아래 함수처럼 return 이 없는 함수를 실행할 때 undefined 를 출력한다. function a() { console.log(123) } 반면 null은 개발자가 명시적으로 변수에 할당한 값 이기 때문에 필요한 곳에서 사용 했구나 라고 인식하면된다. null은 GC 가 쓰지 않는 메모리를 수거할 때 유용하다 https://medium.com/crocusenergy/js-undefined-null-%EC%96%B4%EB%96%A8-%EB%95%8C-%EC%93%B8%EA%B9%8C-8782dc3c35b6

@Bean FTPClient 등록
@RequiredArgsConstructor @Configuration public class CustomFtpClient { private static final Logger logger = LoggerFactory.getLogger(CustomFtpClient.class); private final Common common; @Bean("MFtpClient")//싱글톤으로 쓰고 싶어서 public FTPClient ftpClient() throws Exception { logger.debug("싱글톤"); FTPClient ftpClient = new FTPClient(); ftpClient.setDefaultPort(common.ftpPort); ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out))); int reply; ftpClient.connect(common.ip);// 호스트 연결 reply = ftpClient.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { ftpClient.disconnect(); throw new Exception("Exception in connecting to FTP Server"); } ftpClient.login(common.ftpUser, common.ftpPwd);// 로그인 ftpClient.setFileType(FTP.BINARY_FILE_TYPE); ftpClient.enterLocalPassiveMode(); return ftpClient; } } 객체를 참조하는 클래스를 하나 또 등록하고.. @Component public class FTPUploader { private static final Logger log = LoggerFactory.getLogger(FTPUploader.class); @Qualifier("MFtpClient") @Autowired private FTPClient client; public void uploadFile(String localFileFullName, String fileName, String hostDir) throws Exception { try (InputStream input = new FileInputStream(new File(localFileFullName))) { client.storeFile(hostDir + fileName, input); // storeFile() 메소드가 전송하는 메소드 } } public void disconnect() throws Exception { if (client.isConnected()) { try { client.logout(); client.disconnect(); } catch (IOException f) { f.printStackTrace(); } } } public void CheckAndMakeDirectory(String path) throws Exception{ boolean isExist; isExist = client.changeWorkingDirectory(path); // 없으면 폴더 생성 if(!isExist){ client.makeDirectory(path); } } } 사용법은 아래와 같이 @Async public void ftpSendToCs2(String filePath) throws Exception { // Central Statino sever로 파일 전송 String pattern = Pattern.quote(System.getProperty("file.separator")); logger.debug("filePath: " + filePath); File fileTest = new File(filePath); String fileName = fileTest.getName(); logger.debug("simpleFileName: " + fileName); logger.debug(fileTest.getAbsolutePath()); logger.debug(fileTest.getParent()); logger.debug(fileTest.getParentFile().toString()); logger.debug(fileTest.getCanonicalPath()); String[] parentStrings =fileTest.getParent().split(pattern); logger.debug(parentStrings.toString()); logger.debug(parentStrings[parentStrings.length -2]); //부모 폴더의 부모폴더이름을 뽑아낸다. String folderName = parentStrings[parentStrings.length -2]; ftpUploader.CheckAndMakeDirectory(File.separator + folderName); logger.debug("FTP SEND START"); ftpUploader.uploadFile(filePath, fileName, File.separator+ folderName + File.separator); //파일 전송시 공백있는 이름의 파일을 전송하면 안 됨, 위에서 이미 공백을 다 제거했기 때문. //ftpUploader.disconnect(); logger.debug("FTP SEND DONE"); } FTP 클라이언트를 싱글톤으로 쓰기 위해 @Bean으로 등록했는데, FTP 서버가 먼저 켜지지 않으면 connect를 못하므로 return을 못 시키고, 결국엔 참조하는 FTPUploader가 의존성 에러가 뜨므로, 이걸 사용하지 못할 것 같다.. 다른 좋은 방안은 없을까..

Why is json_encode adding backslashes?
https://stackoverflow.com/questions/10314715/why-is-json-encode-adding-backslashes https://stackoverflow.com/questions/17866996/how-to-access-plain-json-body-in-spring-rest-controller https://stackoverflow.com/questions/45717079/stop-json-stringify-from-adding-escape-characters How to make RestController handle json Strings properly https://stackoverflow.com/questions/38083872/how-to-make-restcontroller-handle-json-strings-properly spring에서 "" 을 붙여서 return 해주기 때문에, / 생김..

Mybatis - pageHelper
https://github.com/pagehelper/Mybatis-PageHelper https://goodteacher.tistory.com/251 https://badstorage.tistory.com/13 custom https://www.bswen.com/2018/06/springboot-mybatis-with-pageHelper.html https://www.cnblogs.com/zyl187110/p/11442897.html 1page: Page{count=true, pageNum=1, pageSize=10, startRow=0, endRow=10, total=100, pages=10, reasonable=false, pageSizeZero=false}[Book(id=101, author=....... 11page: Page{count=true, pageNum=11, pageSize=10, startRow=100, endRow=110, total=100, pages=10, reasonable=false, pageSizeZero=false}[] <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- config params as the following --> <property name="helperDialect" value="oracle" /> </plugin> </plugins> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> @Test public void selectAllPaging() { int perPage = 10; // 몇 페이지에 대한 조회인지 설정 후 조회 PageHelper.startPage(1, perPage); Page<Book> p = bookRepository.pageTest(); //log.info("1 page: {}", p); log.info("1page: " + p); PageHelper.startPage(2, perPage); p = bookRepository.pageTest(); //log.info("2 page: {}", p); log.info("2page: " + p); PageHelper.startPage(10, perPage); p = bookRepository.pageTest(); log.info("10page: " + p); PageHelper.startPage(11, perPage); p = bookRepository.pageTest(); log.info("11page: " + p); PageInfo<Book> books = new PageInfo<Book>(p); log.info("pageInfo: "+books); } PageInfo{pageNum=10, pageSize=10, size=10, startRow=91, endRow=100, total=100, pages=10, list=Page{count=true, pageNum=10, pageSize=10, startRow=90, endRow=100, total=100, pages=10, reasonable=false, pageSizeZero=false}[Book(id=11, author=창현 박종....... .....title=잠수네 아이들의 )], prePage=9, nextPage=0, isFirstPage=false, isLastPage=true, hasPreviousPage=true, hasNextPage=false, navigatePages=8, navigateFirstPage=3, navigateLastPage=10, navigatepageNums=[3, 4, 5, 6, 7, 8, 9, 10]} https://github.com/stella6767/spring-legacy-booklist

Components LifeCycle
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class Basic extends Component { static propTypes = { name: PropTypes.string.isRequired, birth: PropTypes.number.isRequired, lang: PropTypes.string, }; static defaultProps = { lang: 'Javascript', }; static contextTypes = { router: PropTypes.object.isRequired, }; state = { hidden: false, }; componentWillMount() { console.log('componentWillMount'); } componentDidMount() { console.log('componentDidMount'); } componentWillReceiveProps(nextProps) { console.log('componentWillReceiveProps'); } shouldComponentUpdate(nextProps, nextState) { console.log('shouldComponentUpdate'); return true / false; } componentWillUpdate(nextProps, nextState) { console.log('componentWillUpdate'); } componentDidUpdate(prevProps, prevState) { console.log('componentDidUpdate'); } componentWillUnmount() { console.log('componentWillUnmount'); } onClickButton = () => { this.setState({ hidden: true }); this.refs.hide.disabled = true; } render() { return ( <div> <span>저는 {this.props.lang} 전문 {this.props.name}입니다!</span> {!this.state.hidden && <span>{this.props.birth}년에 태어났습니다.</span>} <button onClick={this.onClickButton} ref="hide">숨기기</button> <button onClick={this.context.router.goBack}>뒤로</button> </div> ); } } https://www.zerocho.com/category/React/post/579b5ec26958781500ed9955

초보자를 위한 바닐라 자바스크립트 실습
자바스크립트에 대한 기본적인 조작 방법은 끝났고, 자바스크립트의 이상한 this ES5 ES6 차이점 같은 것은 나중에 다루기로 하고, 일단 실습을 해보자. 뭐든 가장 빨리 배우는 것은 실습이니.. 노마드 코더의 바닐라 JS 무료강의를 듣고 몰랐거나 얘매하게 알던 것을 정리하겠다. css 파일 body{ background-color: aliceblue; } .btn{ cursor: pointer; } h1{ color: burlywood; transition: color 0.5s ease-in-out; } .clicked{ color: slategray; } .form .greetings{ display: none; } .showing { display: block; } .bgImage { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; animation: fadeIn 0.5s linear; } clock.js // ,로 구분 const clockContainer = document.querySelector(".js-clock"), clockTitle = clockContainer.querySelector("h1"); //꼭 js-clock 안에 h1태그가 포함되어있는지 체크하자.. function getTime(){ const date= new Date(); const minutes = date.getMinutes(); const hours = date.getHours(); const seconds = date.getSeconds(); clockTitle.innerText=`${hours < 10 ? `0${hours}`: hours}:${minutes < 10 ? `0${minutes}`: minutes}:${seconds < 10 ? `0${seconds}`: seconds}`; } function init(){ getTime(); setInterval(getTime,1000); //첫번째 인자는 함수, 1초마다 실행 } init(); gretting.js const form = document.querySelector(".js-form"), input = form.querySelector("input"), greetings = document.querySelector(".js-greetings"); const USER_LS = "currentUser", //local storage key 값 SHOWING_CN = "showing"; function saveName(text){ localStorage.setItem(USER_LS, text); //Local Storage = 만료기한 없음, Session Storage = 세션 종료 시 만료. } function handleSubmit(event){ event.preventDefault(); const currentValue = input.value; paintGreetings(currentValue); saveName(currentValue); } function askForName(){ form.classList.add(SHOWING_CN); form.addEventListener("submit", handleSubmit); //submit 리스너 등록 } function paintGreetings(text){ form.classList.remove(SHOWING_CN); greetings.classList.add(SHOWING_CN); greetings.innerText = `Hello ${text}`; } function loadName(){ const currentUser = localStorage.getItem(USER_LS); if(currentUser === null){ askForName(); }else{ paintGreetings(currentUser); } } function init(){ loadName(); } init(); //local storage : 아주 작은 정보를 컴퓨터에 저장시키는 방법 localStorage 는 세션 만료가 없어 브라우저가 닫혀도 지워지지 않는다. 만약 세션이 만료된 경우에 데이터를 지워야 한다면, sessionStorage 에 저장한다. JWT를 여기다 저장시키면 되겠군 ㅎㅎ todo.js const toDoForm = document.querySelector(".js-toDoForm"), toDoInput = toDoForm.querySelector("input"), toDoList = document.querySelector(".js-toDoList"); const TODOS_LS = "toDos"; let toDos = []; //전역 function deleteToDo(event) { const btn = event.target; //이벤트가 발생한 dom. 여기서는 button 태그 const li = btn.parentNode; //부모 태그를 toDoList.removeChild(li); //html part에서 지우고 const cleanToDos = toDos.filter(function(toDo) { //li.id가 아닌 것만 걸러내기 return toDo.id !== parseInt(li.id); // li.id는 string이라 number로 형변환 }); toDos = cleanToDos; //대체 saveToDos(); //로컬스토리지에 저장 } function saveToDos() { //로컬에 저장해서 휘발되지 않게 localStorage.setItem(TODOS_LS, JSON.stringify(toDos)); //자바스크립트 오브젝트를 string으로 변환 } function paintToDo(text) { const li = document.createElement("li"); const delBtn = document.createElement("button"); const span = document.createElement("span"); const newId = toDos.length + 1; //1부터 delBtn.innerText = "❌"; delBtn.addEventListener("click", deleteToDo); //클릭 리스너 등록 span.innerText = text; li.appendChild(delBtn); li.appendChild(span); li.id = newId; toDoList.appendChild(li); //여기까지는 추가했지만 휘발성(새로고침하면 날라감) const toDoObj = { text: text, id: newId }; toDos.push(toDoObj); saveToDos(); } function handleSubmit(event) { //form submit시(Enter) event.preventDefault(); //기본 form 동작을 막아주고 const currentValue = toDoInput.value; //input 박스 안의 내용을 가져온 다음 paintToDo(currentValue); //넘겨준다. toDoInput.value = ""; //다시 비워줌 } function loadToDos() { // userName 없다면 toDolist 출력되지 않음. 새로고침해도 로컬내용보고 불러옴 const loadedToDos = localStorage.getItem(TODOS_LS); if (loadedToDos !== null) { const parsedToDos = JSON.parse(loadedToDos); //자바스크립트 object로 변환 parsedToDos.forEach(function(toDo) { paintToDo(toDo.text); }); } } function init() { loadToDos(); toDoForm.addEventListener("submit", handleSubmit); //리스너 등록 } init(); bg.js const body = document.querySelector("body"); const IMG_NUMBER = 3; function paintImage(imgNumber) { const image = new Image(); //이미지 객체 생성 image.src = `images/${imgNumber + 1}.jpg`; image.classList.add("bgImage"); body.prepend(image); //선택한 요소의 내용의 앞에 콘텐트를 추가합니다. } function genRandom() { // 0~2까지 난수 생성 const number = Math.floor(Math.random() * IMG_NUMBER); //floor = 소수점 이하 버리기 return number; } function init() { const randomNumber = genRandom(); paintImage(randomNumber); } init(); weather.js const weather = document.querySelector('.js-weather'); const API_KEY = 'Your API Code Here'; //가입하기 귀찮다 그냥 넘어가자 const COORDS = 'coords'; // function getWeather(latitude, longitude) { // fetch( // `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEY}&units=metric` //http가 아니라 https로 // ) // .then(function (response) { // return response.json(); // }) // .then(function (json) { // const temperature = json.main.temp; // const place = json.name; // weather.innerText = `🌡${temperature.toFixed(1)}℃ 🇰🇷${place}`; // }); // } function saveCoords(coordsObj) { localStorage.setItem(COORDS, JSON.stringify(coordsObj)); //성공했으면 저장. console.log("coordsObj: "+coordsObj); } function handleGeoSuccess(position) { const latitude = position.coords.latitude; const longitude = position.coords.longitude; const coordsObj = { latitude, longitude, }; saveCoords(coordsObj); //getWeather(latitude, longitude); } function handleGeoError() { console.log('cant access geo location'); //에러시 } function askForCoords() { navigator.geolocation.getCurrentPosition(handleGeoSuccess, handleGeoError); //내 현재 위치를 가져온다. } function loadCoords() { const loadedCoords = localStorage.getItem(COORDS); if (loadedCoords == null) { askForCoords(); } else { const parsedCoords = JSON.parse(loadedCoords); //getWeather(parsedCoords.latitude, parsedCoords.longitude); } } function init() { loadCoords(); } init(); 여기에 잘 정리된 글이 있다. https://velog.io/@devseunggwan/Javascript-%EB%B0%94%EB%8B%90%EB%9D%BC-JS%EB%A1%9C-%ED%81%AC%EB%A1%AC%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-3

springboot multiple datasource config
자동 설정 spring.datasource.url이 모든 Datasource의 url이 된다. 수동 설정 (Java Config) spring.datasource.jdbc-url로 해야 HikariCP가 인식한다. application.properties에 두개의 데이터베이스에 접근하기 위한 정보를 입력해준다. 여기서 중요한 점은 url이 아닌 jdbc-url을 사용해야한다는 것이다. 그 이유는 spring boot 2.0부터 기본으로 사용하는 커넥션 풀이 HikariCP로 변경되었는데 HikariCP에선 databaseURL 설정에서 정의된 변수가 url 이 아닌 jdbcUrl로 정의되어 있기 때문이다. mybatis+jpa https://rangerang.tistory.com/70 https://ehsaniara.medium.com/spring-boot-2-with-multiple-datasource-for-postgres-data-replication-182c89124f54 https://growing-up-constantly.tistory.com/48 JPA https://github.com/jahe/spring-boot-multiple-datasources/blob/master/src/main/java/com/foobar/FooDbConfig.java https://www.computingfacts.com/post/Multiple-Database-Connection-using-Spring-Data-JPA https://mudchobo.github.io/posts/spring-boot-jpa-multiple-database Mybatis https://programmer.group/spring-boot-mybatis-multiple-data-sources.html https://chsoft.tistory.com/entry/Spring-Boot-Multi-Datasource-작성 https://sanghye.tistory.com/26 https://graykang.tistory.com/56 https://aljjabaegi.tistory.com/442 https://dev-overload.tistory.com/30 https://offbyone.tistory.com/381 https://eblo.tistory.com/52 https://jsijsi99.tistory.com/9 https://mdwgti16.github.io/spring boot/spring_boot_mybatis_multi/# 기타 참고 https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto.data-access.configure-two-datasources https://www.fatalerrors.org/a/springboot-db-series-mybatis-multi-data-source-configuration-and-use.html https://jonghyeok-dev.tistory.com/44 https://programmer93.tistory.com/24 https://ukmo.tistory.com/8 https://dotheright.tistory.com/188 https://gigas-blog.tistory.com/122 나의 코드 https://github.com/stella6767/Springboot-MultiDatasource-Mybatis

Immer
yarn add immer//깊은 복사 함수를 안 써도, 깊은 복사할 수 있게끔 만들어주는 라이브러리 immer 사용법 import produce from 'immer'; const nextState = produce(originalState, draft => { //바꾸고 싶은 값 바꾸기 draft.somewhere.deep.inside = 5; }) produce 함수는 두 가지 인자를 받는다. 첫 번째 피라미터는 수정하고 싶은 상태이고, 두 번째 파라미터는 상태를 어떻게 업데이트할지 정의하는 함수이다. (draft)

Redis 설치(M1 Docker)
스프링부트로 Redis 테스트하기 전에 Redis 설치를 해보자. brew로 임베디드 레디스를 설치하지 않는다면, 따로 Docker를 통해서 redis를 설치할 수 있다. 나는 Docker를 통해서 설치하도록 하겠다. Docker 다운로드 https://docs.docker.com/docker-for-mac/apple-silicon/ 애플 M1 preview version을 다운받는다. 언젠가 정식버전이 나오길 기대하면서.. https://www.youtube.com/watch?v=xECyeupbn6c&ab_channel=AJTheEngineer Redis 설치 https://hub.docker.com/_/redis TAG란에서 다른 걸 받아도 된다. docker pull redis:alpine //stable한 alpine 버전 땡겨받는다. 이미지는 설치되었다. 여기서 바로 서버를 구동해도 되지만, redis-cli도 같이 구동해서 통신을 해야 하기 때문에, 2개의 컨테이너를 실행할 것이며, 그 2개간 연결을 위해서 docker-network 구성을 먼저 해야 한다. docker network create redis-net docker network ls // 생성하고 확인 이제 redis를 실행하는데, 앞서 생성한 network 정보를 같이 집어넣고 실행한다. sudo docker run --name my-redis -p 6397:6379 --network reids-net -v /Users/Redis:/data -d redis:alpine redis-server --appendonly yes docker ps // 실행중인 프로세스(컨테이너) 확인 -name: 컨테이너 이름 지정 -p: 포트 설정(기본: 6379) host에 노출될 포트 지정 --network : 네트워크 설정 v: 볼륨 폴더 지정(# 외부 폴더에 데이터 저장소를 두고 싶을 경우) host와 연결할 폴더 지정 -d: 백그라운드로 실행 appendonly yes 옵션은 AOF방식으로 데이터를 저장(참고:Redis Persistence Introduction)하겠다는 의미입니다. 데이터는 기본적으로 /data 하위에 저장되며 외부에서 해당 폴더를 공유함으로써 해당 컨테이너를 지우고 새로 만들어도 해당 volume을 참고하게 하면 동일한 데이터를 유지 할 수 있습니다. error while creating mount source path mkdir operation not permitted. 요렇게 블라블라 뜨면, 볼륨폴더를 만들고, 거기에 도컨 데스크탑에서 파일공유 설정하셈 이제 redis-cli로 해당 redis server에 접속하자 docker run -it --network reids-net --rm redis:alpine redis-cli -h my-redis exit //빠져나가기 —rm: 실행할 때 컨테이너 아이디가 존재하면 삭제 후 run h 뒤에 붙은 컨테이너 명으로 redis-cli를 실행하여 redis server에 접속한다. redis-server 구동할때, -p 옵션으로 host 에 포트를 노출했기때문에, redis 가 설치된 로컬 pc 에서도 접속이 가능하다. https://emflant.tistory.com/235 https://blog.naver.com/semtul79/222235108317 https://jistol.github.io/docker/2017/09/01/docker-redis/ 추가 설정이 필요하다면... 레디스 서버 포트 변경 Docker용 redis.conf 파일을 만든다. path는 아무렇게나해도 된다. 작성하지 않으면 기본 설정을 따라간다. 설정 옵션 : http://redisgate.kr/redis/configuration/param_daemonize.php /redis/redis.conf #daemonize no # yes로 변경시 구동되지 않음 # bind 127.0.0.1 protected-mode no port 6000 #변경하고자 하는 포트 #logfile "redis.log" #이 옵션 사용시 파일로 로그가 저장되고 프롬프트는 노출되지 않음 #workingdir을 지정 #dir /data # SECURITY requirepass changeme # CLIENTS maxclients 10000 해당 conf파일을 지정해서 실행 : volume 지정 docker run --rm --name redis -p 6000:6000 -v /Users/jiyeonpark/Desktop/redisvolume/redis.conf:/usr/local/etc/redis/redis.conf -d redis:latest redis-server /usr/local/etc/redis/redis.conf --appendonly yes Config 설정을 Dockerfile 내에서 설정하는 법 https://yongho1037.tistory.com/699 log 확인docker logs redis 변경된 포트로 client 접속docker run -it --link redis:latest --rm redis redis-cli -h redis -p 6000 테스트> auth changeme #[redis.conf에서 입력한 비밀번호] > info Shell로 Docker 리눅스에 접속하기docker ps docker exec -it myredis /bin/bash 출처: https://littleshark.tistory.com/68

Mysql && Oracle 더미데이터 삽입 프로시저
Mysql 방법1. PROCEDURE 이용 주의사항 : CREATE PROCEDURE ~ END 까지 커서로 전체 선택해서 Ctrl + Enter 눌러야 생성됨. 출처: https://insanelysimple.tistory.com/112 [Simple is best] DELIMITER $$ DROP PROCEDURE IF EXISTS loopInsert$$ CREATE PROCEDURE loopInsert() BEGIN DECLARE i INT DEFAULT 1; DECLARE gen varchar(100) default "남"; WHILE i <= 100 DO if i%2 = 0 then SET gen = '여'; ELSE SET gen = '남'; END IF; INSERT INTO patient(name, gender, height,weight) VALUES(concat('홍길동 ',i), gen, 180, 75); SET i = i + 1; END WHILE; END; CALL loopInsert() select * from patient p ; ALTER TABLE patient AUTO_INCREMENT = 1; delete from patient; Oracle CREATE OR REPLACE PROCEDURE insertDummyData as i NUMBER := 1; gen NUMBER :=1; BEGIN WHILE i <= 150 LOOP if MOD(i,2) = 0 then gen := 1; ELSE gen := 0; END IF; INSERT INTO patient(pid, patientUserId, firstname,lastname, gender,age, height,weight, "comment") VALUES(patient_seq.nextval , concat('patient ',i) ,concat('GILDONG ',i), 'HONG',gen, 28, 180, 75,'dummy data'); i := i + 1; END LOOP; END insertDummyData; 인자가 없을 시에는 () 생략 후, 변수 선언부에 AS, oracle은 % 연산자가 없으니 MOD() 사용.

Redux Action Tip
라이브러리 하나 파악하기 참 힘들다.. https://redux-actions.js.org/api/createaction 전달받은 파라미터가 여러 개일 때는 객체를 만들어서 파라미터에 넣어 주면 된다. export const userId = 1; export const postId = 2; dispatch(getPostAction({ userId, postId })); //객체로 전달 export const getPostAction = createAction(GET_POST_REQUEST, ({ userId, postId }) => ({ userId, postId })); /* 결과 : { type: 'GET_POST_REQUEST' payload: { userId:1, postId:2, } } */ saga와 연동해서 잘 동작하는지 확인 export const detail = ({ userId, postId }) => { // const queryString = qs.stringify({ // page, // username, // tag, // }); //return client.get(`/api/posts?${queryString}`); return console.log('이게 되냐?', userId, postId); }; 객체로 받고, 인수로 전달할 수도 있다.. it('should return a map of camel-cased action types to action creators', () => { const { actionOne, actionTwo } = createActions({ ACTION_ONE: (key, value) => ({ [key]: value }), ACTION_TWO: ({ first, second }) => ([first, second]) }); expect(actionOne('value', 1)).to.deep.equal({ type: 'ACTION_ONE', payload: { value: 1 } }); expect(actionTwo({ first: 'value', second: 2 })).to.deep.equal({ type: 'ACTION_TWO', payload: ['value', 2] }); }); https://backback.tistory.com/316?category=801894

Spring 서버에서 REST API 호출
http 프로토콜로 통신하는법 RestTemplate https://www.baeldung.com/spring-resttemplate-post-json https://recordsoflife.tistory.com/360 public void reqThreadStart(String jsonData) { //8080 포트 서버임, 8081로 전송 logger.debug("받은 jsonData: " + jsonData); RestTemplate rt = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); //MultiValueMap<String, String> parames = new LinkedMultiValueMap<>(); //parames.add("jsonData", jsonData); HttpEntity<String> request = new HttpEntity<>(jsonData, headers); //ResponseEntity response = rt.exchange("http://localhost:8081/test", HttpMethod.GET, request, String.class); String response = rt.postForObject("http://localhost:8081/test", request, String.class); ObjectMapper objectMapper = new ObjectMapper(); //json 데이터 전송 System.out.println(response); } @PostMapping("/test") //받는 8081 서버 public String 체크(@RequestBody String jsonData) { System.out.println("들어왔나?"); System.out.println(jsonData); return jsonData; } Unirest https://easybrother0103.tistory.com/103 WebClient https://www.baeldung.com/spring-webclient-resttemplate

unknown collation 'utf8mb4_0900_ai_ci' mariadb
참고 https://na0-0.tistory.com/24

시큐리티 비밀번호 검증
Spring Security의 사용자 비밀번호 검사 스프링 시큐리티 예제를 보며 개발하다 보면 UserDetailsService 인터페이스의 loadUserByUsername(String username)을 구현해서 사용자 정보를 DB에서 조회하고 반환한다. 하지만 비밀번호를 체크하는 코드는 없다. 그래도 잘못된 비밀번호를 입력하면 로그인에 실패한다. 내가 작성한 코드에는 없지만 어디에선가 비밀 번호 체크를 하고 있는 것이다. 비밀 번호는 어디에서 체크할까? DaoAuthenticationProvider 컨트롤러에서 AuthenticationManager.authenticate(Authentication)을 호출하면 스프링 시큐리티에 내장된 AuthenticationProvider의 authenticate() 메서드가 호출되는데, 이 중에서 DaoAuthenticationProvider.additionalAuthenticationChekcs(UserDetails, UsernamePasswordAuthenticationToken) 메서드에 다음과 같은 코드가 있다. String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); }` matches 메서드를 활용해 암호화된 비번 검증할 수 있다. 시큐리티를 사용 중, 회원사 비번 수정 기능을 부여하는데 있어서 기존 비밀번호를 한 번 더 테스트해야 될 필요가 있었다. 하지만, 비번을 암호화하면 매번 다른 랜덤키로 인코딩 되기 때문에, 기존 방식으로 검증은 불가능하고, 인코더 객체의 matches 함수를 활용해, 검증할 수 있었다. @Transactional public Integer memberUpdatePassword(MemberReqDto reqDto) { Member member = memberMapper.findByMemberId(reqDto.getId()); Integer result; //매번 다른 랜덤키를 부여하기 때문에 떠로 디코딩 작업필요 if (bCryptPasswordEncoder.matches(reqDto.getCheck_member_password(), member.getMember_password())) { log.info("일치합니다."); String updatePassword = bCryptPasswordEncoder.encode(reqDto.getMember_password()); result = memberMapper.updateMemberPassword(reqDto.getId(), updatePassword); } else { log.info("불일치합니다."); result = 0; } return result; } 참고 https://github.com/HomoEfficio/dev-tips/blob/master/Spring Security의 사용자 비밀번호 검사.md

Spring Boot Requsets, 로깅 찍기
https://stackoverflow.com/questions/33744875/spring-boot-how-to-log-all-requests-and-responses-with-exceptions-in-single-pl https://stackoverflow.com/questions/48301764/how-to-log-all-the-request-responses-in-spring-rest Spring Boot has a modules called Actuator, which provides HTTP request logging out of the box. https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints 나는 CommonsRequestLoggingFilter 사용 https://www.baeldung.com/spring-http-logging 스프링부트 사용 예시 server: port: 8080 servlet: context-path: / encoding: charset: utf-8 enabled: true logging: level: "[org.springframework.web.filter.CommonsRequestLoggingFilter]": debug spring: servlet: multipart: max-file-size: 10MB @Configuration public class RequestLoggingFilterConfig { @Bean public CommonsRequestLoggingFilter logFilter() { CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); filter.setIncludeQueryString(true); filter.setIncludePayload(true); filter.setMaxPayloadLength(640000); filter.setIncludeHeaders(true); filter.setIncludeClientInfo(true); filter.setAfterMessagePrefix("REQUEST DATA : "); return filter; } } @GetMapping("/send") public void sendMultipart() throws Exception { String filePath = "C:\\Users\\songn\\Downloads\\Telegram Desktop\\unirest.png"; Map<String, Object> params = new HashMap<String, Object>(); params.put("id", "AIBAA"); File file = new File(filePath); logger.info("file: " + file); HttpResponse<String> response = Unirest.post("http://localhost:8080/resp") .queryString(params) .field("img", new File(filePath)) .asString(); //헤더는 자동 multipart-form data String responseJson = response.getBody(); logger.info("## CrescomAiService.insUpload() responseJson={}", responseJson); } @PostMapping(value = "/resp", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) public String respMultipart(@RequestParam("img") MultipartFile img, HttpServletRequest req){ logger.info("multipartFile: " + img ); //logger.info("req: " + req.toString()); String id = req.getParameter("id"); // img.getBytes() 바이트로 변환해서 파일 저장하면 될것 같습니다. logger.info("id: "+ id); return "ok"; } 2021-10-13 16:11:51.728 DEBUG 17284 --- [nio-8080-exec-5] o.s.w.f.CommonsRequestLoggingFilter : Before request [POST /ins_upload%20, client=127.0.0.1, headers=[user-agent:"PostmanRuntime/7.28.4", accept:"*/*", postman-token:"93c1a335-07eb-47fc-a9c9-70d0938eb7a5", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"1550102", Content-Type:"multipart/form-data;boundary=--------------------------582909627413188773404514;charset=UTF-8"]] 2021-10-13 16:11:51.755 DEBUG 17284 --- [nio-8080-exec-5] o.s.w.f.CommonsRequestLoggingFilter : REQUEST DATA : POST /ins_upload%20, client=127.0.0.1, headers=[user-agent:"PostmanRuntime/7.28.4", accept:"*/*", postman-token:"93c1a335-07eb-47fc-a9c9-70d0938eb7a5", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"1550102", Content-Type:"multipart/form-data;boundary=--------------------------582909627413188773404514;charset=UTF-8"]] 2021-10-13 16:12:02.813 DEBUG 17284 --- [nio-8080-exec-6] o.s.w.f.CommonsRequestLoggingFilter : Before request [POST /resp, client=127.0.0.1, headers=[user-agent:"PostmanRuntime/7.28.4", accept:"*/*", postman-token:"6be1c630-3e32-4a95-a2c5-482ec4097dcf", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"1550102", Content-Type:"multipart/form-data;boundary=--------------------------246468667497882729744630;charset=UTF-8"]] 2021-10-13 16:12:02.831 INFO 17284 --- [nio-8080-exec-6] c.e.cresomtest.web.ImageController : multipartFile: org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@49d0ef9b 2021-10-13 16:12:02.832 INFO 17284 --- [nio-8080-exec-6] c.e.cresomtest.web.ImageController : id: AIBAA 2021-10-13 16:12:02.833 DEBUG 17284 --- [nio-8080-exec-6] o.s.w.f.CommonsRequestLoggingFilter : REQUEST DATA : POST /resp, client=127.0.0.1, headers=[user-agent:"PostmanRuntime/7.28.4", accept:"*/*", postman-token:"6be1c630-3e32-4a95-a2c5-482ec4097dcf", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"1550102", Content-Type:"multipart/form-data;boundary=--------------------------246468667497882729744630;charset=UTF-8"]] 요런식으로 로깅 확인.. 프론트엔드 개발자는 axios의 interceptor 이용해서 로깅 찍는 거랑 비슷하게 생각하면 됨.