반응형

Main 수정

저번 포스팅에서 만들었던 demo-main 모듈의 Main 클래스명을 변경하고 어노테이션을 달아주었다

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

 

해당 클래스 위치는 아래와 같다

application.yml 추가

그리고 나는 admin 과 main 모듈을 각각 실행시키고싶어서 port 설정을 추가해야하는 상황이라 application.yml을 추가해주었다

server:
  port: 8080

 

위치는 아래와 같이 resources 폴더를 만들고 그 안에 application.yml 을 추가하였다

 

Controller 생성

간단하게 테스트하기위하여 Restcontroller 로 생성하였다

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/main")
public class MainController {
    @ResponseBody
    @GetMapping("")
    public ResponseEntity index() {
        String result = "SUCCESS!!";

        return ApiResponseEntity
                .data()
                .put("result", result)
                .ok();
    }
}

 

위치는 아래와 같이 위치하면 된다

 

위 코드에 리턴타입이 ApiResponseentity 라고 되어있는데 해당 클래스는 core에 만들었다

 

Core 공통모듈 사용

 

이렇게 core 쪽에 내가 사용하고싶은 클래스를  추가하고 다른 모듈에서 자유롭게 불러서 사용가능하다

 

참고로 root 경로의 build.gradle 의  설정에 따라 타 모듈 -> core 연결이 가능한것이기 때문에

core -> 타 모듈 의 클래스를 import하는것은 불가능하다

728x90
반응형

 

spring boot 프로젝트 생성

 

intellij 에서 하든 spring 에서 하든.. 편한 방법으로 프로젝트를 생성한다

 

혹시 모르는 사람을 위하여 링크를 남겨둠

spring Initializr :  https://start.spring.io/

 

 

프로젝트 구조 수정

우선 root 경로의 src 폴더를 삭제한다

 

그리고 root 폴더 -> 우클릭 -> new -> Module 을 추가해준다

 

이때 내가 원하는 모듈갯수만큼 추가하면 된다

 

module name을 설정해주고

Build System 이 기본적으로 Maven 에 가있을건데 gradle로 해주자

아래 group Id 까지 설정해주면 완료

 

나는 총 3개의 모듈을 생성하였다

demo-main -> 사용자단 api 

demo-admin -> 관리자단 api

demo-core -> 공통 로직

 

 

전부 생성하게 되면 이런식의 구조를 확인할 수 있을것이다.

 

모듈 관계 설정

settings.gradle 설정

root 폴더 경로에 위치한 settings.gradle 에 모듈 설정을 추가해준다

rootProject.name = 'demo'
include 'demo-main', 'demo-admin', 'demo-core'

 

build.gradle 설정

모듈 별 공통 libraries 들을 설정해주고 각 모듈 별 관계성을 정리해주자

 

아래 내가 작업한 코드인데 눈여겨볼점은 project 설정부분이다

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.2'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	implementation {
		extendsFrom annotationProcessor
	}
}

subprojects {
	apply plugin: 'java'
	apply plugin: 'java-library'
	apply plugin: 'org.springframework.boot'
	apply plugin: 'io.spring.dependency-management'

	configurations {
		compileOnly {
			extendsFrom annotationProcessor
		}
	}

	repositories {
		mavenCentral()
	}

	// 관리하는 모듈의 공통 dependencies
	dependencies {
		implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
		implementation 'org.springframework.boot:spring-boot-starter-web'
		compileOnly 'org.projectlombok:lombok'
		developmentOnly 'org.springframework.boot:spring-boot-devtools'
		implementation "org.mariadb.jdbc:mariadb-java-client:2.1.2"
		implementation 'org.springframework.boot:spring-boot-starter-validation'
		annotationProcessor 'org.projectlombok:lombok'
		testImplementation 'org.springframework.boot:spring-boot-starter-test'

	}

	test {
		useJUnitPlatform()
	}
}

project(':demo-main') {
	jar {
		archivesBaseName = 'demo-main'
	}
	dependencies {
		//컴파일 시 core 로드
		compileOnly project(':demo-core')
	}
}

project(':demo-admin') {
	jar {
		archivesBaseName = 'demo-admin'
	}
	dependencies {
		//컴파일 시 core 로드
		compileOnly project(':demo-core')
	}
}

project(':demo-core') {
	//bootJar로 패키징 할 필요 없음 Main() 메서드 필요없기때문
	bootJar { enabled = false }
	jar { enabled = true }
}

clean {
	delete file('src/main/generated')
}

 

 

configurations 에 compileOnly가 꼭 있어야 멀티모듈 dependencies 의 compileOnly 설정을 사용할 수 있다

subprojects {
	apply plugin: 'java'
	apply plugin: 'java-library'
	apply plugin: 'org.springframework.boot'
	apply plugin: 'io.spring.dependency-management'

	configurations {
		compileOnly {
			extendsFrom annotationProcessor
		}
	}
}

 

 

 

개별 모듈 build.gradle 설정

 

공통로직 모듈을 제외한 다른 모듈의 경우 아래와 같이 작성하면 된다

dependencies 에 core 모듈을 implementation 에 추가한다

plugins {
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation project(':demo-core')

    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
}

 

core 모듈은 build.gradle 없애놓고 저장!

 

여기까지하면 모듈간 관계설정 세팅이 완료되었다.

 

다음 포스팅에서 실제로 활용해보자

 

728x90
반응형

 

Preferences -> Android -> SDK location 설정

 

Project -> 버전선택

728x90
반응형

 

이클립스..안드로이드....

 

jar 파일 import 했는데 적용이 안됐다

 

하나씩 확인해보자

 

1. jar 파일 까보면 .class 가 아닌 .java 로 되어있는지

2. libs 폴더에 넣고 해당 jar 파일 우클릭 -> build path -> add build path 했는지

3. 프로젝트 우클릭 -> properties -> Java Build Path -> Order and Export 에서 해당 jar 파일 체크 되어있는지

 

나는 마지막 3번.. import 시켜놧더니만 이클립스 툴 자체의 properties 에서 체크를 안해놔서 import가 안됐었다..

인텔리제이만 써가지고..;; 이런경우보니까 너무 당황스럽다 ㅠㅠ

 

참고로 Android private libraries 체크해제 안하면 에러뜬다; 쟤도 꼭 체크해제 해주길

728x90
반응형

 

그런일이 일어나서는 안되겠지만

혹시 이클립스로 안드로이드를 작업하는 일이 생긴다면...

 

아래와 같은 오류메세지를 볼수도있다

 

Android Dx Error1, Failed to convert to Dalvik format

 

그럴때는 프로젝트 우클릭 -> Properties -> Java Build Path -> Order and export 클릭

 

Android Private Libraries 체크를 해제해주자

 

728x90
반응형

 

 

react 프로젝트에서 svg를 import 시키고 컴포넌트를 아래와 같이 추가하였다

import {useRouter} from "next/router";

import GoogleSignIn from '@/assets/googleIcon.svg'

export const JoinGoogle = () => {

    function socialLoginGoogle() {
        ///////
    }

    return (
        <>
            <button onClick={socialLoginGoogle}>
                <GoogleSignIn />
                구글로 가입하기
            </button>
        </>
    )
}


별다른 문제가 없을거라 생각했는데, 오류가 발생하였다

 

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

 

터미널 로그에서는 <GoogleSignIn/>을 확인하라고 보여진다

 

Check your code at JoinGoogle.tsx:21.

 

확인해보니, svg파일을 사용하려면 별도의 플러그인을 install 해야한다고 함...

 

해결방법

 

1. install svgr webpack 

npm install -D @svgr/webpack

 

터미널에서 svgr webpack 을 다운받는다

 

2. next.config.js 수정

/** @type {import('next').NextConfig} */
const nextConfig = {

....
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    })
    return config
  },
  
  .....
}

module.exports = nextConfig

 

webpack 설정을 추가해준다

728x90
반응형

localhost:3000 이라는 포트로 만들어진 react 프로젝트에서

다른 포트번호로 실행된 백단 프로젝트를 연결할때 api cors error 를 필연적으로 만나게된다.

 

해결방법은 간단하다

 

1. next.config.js 수정

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false, //true 면 next dev 실행할때 useEffect 2번씩 실행
  swcMinify: true,
  eslint: {
    ignoreDuringBuilds: true,
  },
  compiler: {
    styledComponents: true,
  },
  images: {
    unoptimized: true,
    domains: ['picsum.photos', 'img.dmitory.com', 'image.edaily.co.kr'],
  },
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    })
    return config
  },
  //api cors 에러 프록시 설정
  async rewrites() {
    return [
      {
        source: '/api-aaa/:path*',
        destination: 'http://localhost:8080/api-aaa/:path*',
      },
      {
        source: '/api-bbb/:path*',
        destination: 'http://localhost:8090/api-bbb/:path*',
      },
      {
        source: '/api-ccc/:path*',
        destination: 'http://localhost:8443/api-ccc/:path*',
      },
      {
        source: '/upload/:path*',
        destination: 'http://localhost:8443/upload/:path*',
      },
    ]
  },
}

module.exports = nextConfig

 

 

rewrites 안쪽에 내가 통신하려고 하는 서버의 포트번호를 넣어주면 된다

나의 경우 멀티모듈을 사용했기때문에 저렇게 많은 포트가 필요했다

 

source 설정

지난 포스팅에 있는것처럼 api 호출할때 아래와 같은식으로 호출되는 부분이 있다고 한다면, 

getMenu = async ()=> {
    return await client.get('/api-aaa/menu/list')
}

 

/api-aaa/ 를 prefix로 삼고 api-aaa 로 통신하는 모든 uri들은 포트 8080으로 가게끔 경로를 틀어준다는 의미이다

 

destination 설정

만약 로컬에서 프론트/백단 소스를 모두 띄운다면 위 코드는 문제가 없겠지만

 

로컬에서 프론트 실행하고, 다른 서버에서 백단을  실행하거나

프론트/백단 소스코드를 모두 다른 서버(aws, linux 등..) 에서 실행한다면 destination 을 변경해야한다

...

async rewrites() {
    return [
      {
        source: '/api-aaa/:path*',
        destination: 'http://서버IP:8080/api-aaa/:path*',
      },
      {
        source: '/api-bbb/:path*',
        destination: 'http://서버IP:8090/api-bbb/:path*',
      },
      {
        source: '/api-ccc/:path*',
        destination: 'http://서버IP:8443/api-ccc/:path*',
      },
      {
        source: '/upload/:path*',
        destination: 'http://서버IP:8443/upload/:path*',
      },
    ]
  },

...

 

localhost 부분을 서버IP로 바꿔주면 된다.

 

 

2. 백단 소스코드수정

만약 WebConfig 를 설정하는 소스코드가 있다면 해당부분을 수정하고 없다면 새로만들어주자

@Configuration 으로 전체검색했을때 없으면 없는것

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Value("${config.server-url}")
    String serverUrl;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://" + serverUrl + ":3000") // 프론트의 주소와 포트번호
                .allowedMethods("GET", "POST")
                .allowCredentials(true);
    }

}

 

 

나는 get 과 post만 열어줬고 이 설정만 추가하면 잘될것!!

 

혹시 allowedOrigins에 다른 ip주소도 추가해야한다면 아래와같이 콤마로 추가할수있다

.allowedOrigins("http://" + serverUrl + ":3000", "http://어쩌고ip:3000")

 

 

그래도 안된다면, spring security 사용하고있는경우 security 설정에서 막고있는게 아닌지 확인해야 한다.

 

그래도 안된다면!! IP주소 혹은 API PATH 오타가 있지는 않은지 확인해보자

728x90
반응형

 

 

 

 [webpack.cache.PackFileCacheStrategy] Caching failed for pack: Error: Cannot find module 'mini-css-extract-plugin/dist/CssDependency'

 

.next 의 cache 폴더 삭제

728x90
반응형

 

 

나의 경우는 백단 서버가 이미 존재하여, URI로 요청하는 방식이므로 axios를 사용했다

axios 사용법에 앞서, 기본적인 react 문법을 훑고 지나가자

 

기본 react 문법

아래는 임시로 작성한 Mypage 이다

import {NextPageWithLayout} from "@/pages/_app";
import Layout from "@/components/layout/Layout";
import {useState} from "react";

interface itemListProps {
    id: string
    title: string
}

const MyPage: NextPageWithLayout = () => {

    //기본적인 변수 선언법
    const [nickname, setNickname] = useState<string>('')
    const [age, setAge] = useState<number>(0)
    const [isModalOpen, setIsModalOpen] = useState(false)
    const [itemList, setItemList] = useState<itemListProps[]>([])

    return(
        <div> mypage </div>
        <div>
        	{itemList.map((item, index) => (
                <div key={item.id}>{item.title}</div>
            ))}
    )
}

MyPage.getLayout = function getLayout(page) {
    return <Layout>{page}</Layout>
}

export default MyPage

 

useState

변수선언은 useState 를 이용하여 선언한다. 우측 괄호 안에 값을 넣는 것으로 초기화 가능하다

또한 List 선언의 경우 type을 정해서 itemListProps 라는 type을 정해서 리스트화 시켜줬다.

 

parameter type

특히 api와 통신하는 값을 받는 리스트의 경우 위와 같이 type을 interface로 선언해주고 사용하자.

n명의 개발자가 동시에 개발할때는 type이 정해져있지않다면 중구난방 난리가 날것이기 때문...

 

자세한 상황은 아래 api 연결할때 설명하겠다

Map

리스트의 경우 map 을 이용하여 풀어낼 수 있다.

첫번째 인자로 객체가 들어가고, 두번째는 index가 오게되는데 map 안쪽에 첫번째 요소에 key 속성이 꼭 있어야한다

만약 아래와 같이 코드를 만들경우 에러가 날것이다

return(
    <div> mypage </div>
    <div>
        {itemList.map((item, index) => (
            <div>{item.title}</div>
        ))}
)

 

key를 꼭 붙여주도록 하자

 

 

install axios

프로젝트의 루트위치에서 터미널을 실행하고 아래와 같은 커맨드를 입력한다

$ npm install axios

 

pakage.json, package-lock.json 파일이 자동으로 수정될것이다

 

axios 설정

루트폴더에 api 폴더를 생성하고 client.tsx 파일을 생성하자

 

이곳에 axios 의 모든 설정을 세팅할것이다

get, post, multipartfile을 같이 보낼경우 3가지의 경우를 연결하겠다

 

▼client.tsx

import axios from 'axios';


class Client {

    async get(url: string, params?: any) {
        try {
            const headers = {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest',
            }

            const response = await axios.get(url, {
                params: params,
                headers: headers,
            });
            return response.data;
        } catch (error) {
            console.error(error);
            throw error;
        }
    }

    async post(url: string, params?: any) {
        try {
            const headers = {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest',
            }

            const response = await axios.post(url, params, {
                headers: headers,
            });
            return response.data;
        } catch (error) {
            console.error(error);
            throw error;
        }
    }

    async postMultipartFile(url: string, params: FormData) {
        try {
            const headers = {
                'Content-Type': 'multipart/form-data',
                'X-Requested-With': 'XMLHttpRequest',
            }
            const response = await axios.post(url, params, {
                headers: headers,
            });
            return response.data;
        } catch (error) {
            console.error(error);
            throw error;
        }
    }

}

export const client = new Client();

 

이렇게 소스를 따로 뗀 이유는 api 통신이 필요한 프로젝트 내 모든 페이지에서 별도로 연결을 해주어야하기 때문이다

가독성에 안좋고 스프링시큐리티 등의 이유로 인하여 header에 특정 텍스트/쿠키 등을 태워서 보내야 하는 경우 이곳에서 한번에 처리가 가능하다.

 

axios 적용 테스트

자 이제 실제 페이지에서 사용해봅시다

const MyPage: NextPageWithLayout = () => {

    //기본적인 변수 선언법
    const [nickname, setNickname] = useState<string>('')
    const [age, setAge] = useState<number>(0)
    const [isModalOpen, setIsModalOpen] = useState(false)
    const [itemList, setItemList] = useState<itemListProps[]>([])

    useEffect(() => {
        client.get("/api/어쩌고저쩌고")
            .then((response) => {
                console.log(response)
            }).catch((error) => {
                console.log(error)
            })
    },[])
    
    return(
        <div> mypage </div>
    )
}

 

mypage에 진입하자마자 useEffect 가 실행되고 만들어놓은 axios get 통신을 진행하게된다

그런데 보면... 코드상에서 직접 api path 를 입력하게되는데 딱봐도..너무불안하다

실수로 오타라도 나면 url 호출이 안되면서 에러가 빡빡 생길 조짐이 보인다...

그리고 path 가 일괄변경되면 이렇게 호출한 api 로직부분을 하나하나 찾아서 고쳐줘야한다..

 

그래서 api 통신부분을 따로 빼기로했다

axios 실제 적용

 

 

api 폴더 안에 패키지 하나 + tsx 파일 하나 선언해주자

보통 내가 쓰는 도메인 위주로 하면된다

user 관련 api 면 user.. main 페이지 관련 api 면 main.. menu.. category..등등

import {client} from "@/api/client";

class MyPageApi {
    /**
     * ㅇㅇㅇ페이지 :: ㅁㅁㅁ 조회
     */
    test1 = async () => {
        return await client.get('/api/어쩌고주소')
    }

    /**
     * ㅇㅇㅇ페이지 :: ㅁㅁㅁ 조회
     * @param title
     */
    test2 = async (title:string) => {
        return await client.get('/api/어쩌고주소',title)
    }

    /**
     * ㅇㅇㅇ페이지 :: ㅁㅁㅁ 수정
     */
    test3 = async () => {
        return await client.post('/api/어쩌고주소')
    }

    /**
     * ㅇㅇㅇ페이지 :: ㅁㅁㅁ 삭제
     * @param title
     */
    test4 = async (title:string) => {
        return await client.post('/api/어쩌고주소', title)
    }
}

export const myPageApi = new MyPageApi();

 

이렇게 모아두면 설령 api path가 변경되는 일이 있더라도, 한번에 처리하기 쉽다!

실제 페이지 코드에서는 높은 가독성까지!

 

이걸 실제 코드에 적용시키면

import {NextPageWithLayout} from "@/pages/_app";
import Layout from "@/components/layout/Layout";
import {useEffect, useState} from "react";
import {myPageApi} from "@/api/myPage/myPageApi";

interface itemListProps {
    id: string
    title: string
}

const MyPage: NextPageWithLayout = () => {

    useEffect(() => {
      myPageApi.test4('test')
          .then((response:itemListProps) => {
              console.log(response)
          })
          .catch((error) => {
              console.log(error)
          })
    },[])

    return(
        <div> mypage </div>
    )
}

MyPage.getLayout = function getLayout(page) {
    return <Layout>{page}</Layout>
}

export default MyPage

 

파라미터만 url 별도입력없이 파라미터만 던지는 가독성이 깔끔한 로직이 완성된다

728x90
반응형

 

강의를 듣다보니 예전 안드로이드스튜디오랑은 좀 다른게 생겨서 메모해놓는다

empty Activity 만들고싶은데 워낙 선택지가 많아서 헷깔렸다

 

방법1. 바로선택

방법2. Gallery 보고 선택

New > Activity > Gallery 선택

 

 

Empty Views Activity 선택

 

Activity 이름 설정

이때 Activity Name 과 Layout Name을 잘 맞춰야한다

 

커서를 앞으로 이동하여 Main을 지우고 다른 문자로 바꾸면 괜찮지만, 전체삭제한경우 activity_가 prefix로 붙지않는 경우도 있기 때문이다

 

 

혹시나 activity 생성하고나서 setContentView(R.layout.레이아웃이름) 부분이 제대로 import안되더라도 걱정하지말자

일시적으로 import 인식을 못하는것같다.

 

한번 실행해보면 오류없이 실행됨!

 

728x90

+ Recent posts