Fullstack-Study-241204-250625

커리큘럼(12-30/변경)

01. Java
02. git
03. Database 
04. Jsp [Server]

05. HTML,CSS 
07. JS
06. 미니프로젝트-2W

08. SpringFramework , SrpingBoot 
19. 중간프로젝트 (1M) (v)
10. Linux 명령어 
11. AWS 클라우드 
12. React JS [Front-end]
13. DevOps - Docker
14. App - Android
15. 최종프로젝트 (1M)

팀프로젝트 SWS(04/08 ~ 05/04)

우리 아이 안심 귀가 서비스

사용기술

프론트엔드(Frontend)

백엔드(Backend)

Spring Boot

1. SockJS
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/wss").setAllowedOriginPatterns("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}
2. Scheduler
// 매일 새벽 0시에 실행
@Scheduled(cron = "0 0 0 * * *")
@Transactional
public void scheduledUpdate() {
    int result = 0;
    // 오전, 오후에 맞는 driveInfoKey받기
    List<Integer> am = getDriveInfoKey("오전");
    List<Integer> pm = getDriveInfoKey("오후");
    // record테이블에 추가
    registRecordDailyAM(am);
    registRecordDailyPM(pm);
    // recordMatch테이블에 필요한 데이터 가져오기(km_key, record_key)
    List<RecordMatchVO> list = getRecordMatachInfo();
    // recordMatch에도 추가
    registRecordMatchDaily(list);
    System.out.println("기사 운행정보 추가");
}
3. Interceptor
// # Interceptor(세부 조건 설정)
@Component // webConfig에서 bean으로 등록안해도됨 (자동 빈 등록)
public class FirstVisitInterceptor implements HandlerInterceptor {
    // 첫 화면시 무조건 로딩화면
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        HttpSession session = request.getSession();
        String uri = request.getRequestURI();

        System.out.println("firstVisit: " + session.getAttribute("firstVisit"));

        // 세션에 방문 여부가 없다면
        if (session.getAttribute("firstVisit") == null) {
            session.setAttribute("firstVisit", true);

            // 로딩 페이지 자체는 리다이렉트하지 않도록 예외 처리
            if (!uri.equals("/loading")) {
                response.sendRedirect("/loading");
                return false;
            }
        }

        return true; // 이미 방문했으면 계속 진행
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("컨트롤러 실행 후 인터셉터 동작");
    }
}

// # WebConfig파일(경로 설정)
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private FirstVisitInterceptor firstVisitInterceptor;


    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(firstVisitInterceptor)
                .addPathPatterns("/**")// 모든 경로에 적용
                .excludePathPatterns(
                "/loading","/css/**", "/js/**", "/img/**", "/favicon.ico")
                .excludePathPatterns("/.well-known/**");



        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") // 모든 경로에 적용
                .excludePathPatterns(
                        "/loading",            // 첫 방문 페이지
                    "/user/*", // 유저로그인관련
                    "/css/**", "/js/**", "/img/**", "/favicon.ico") // 정적 리소스 제외); // 리다이렉트 대상은 제외!
                .excludePathPatterns("/.well-known/acme-challenge/**");

    }
}

API

데이터베이스(DataBase)

@Override
public void registKinderAPI() {
    int pageSize = 1000;
    int startIndex = 1;
    // 1. 총 개수 확인
    String countUrl = String.format(
            "http://openapi.seoul.go.kr:8088/%s/json/ChildCareInfo/1/1/",
            apiKey
    );
    JsonNode countRoot = restTemplate.getForObject(countUrl, JsonNode.class);
    int totalCount = countRoot.path("ChildCareInfo").path("list_total_count").asInt();
    System.out.println("총 어린이집 수: " + totalCount);
    // 2. 페이징 돌면서 가져오기
    while (startIndex <= totalCount) {
        int endIndex = Math.min(startIndex + pageSize - 1, totalCount);
        String url = String.format(
                "http://openapi.seoul.go.kr:8088/%s/json/ChildCareInfo/%d/%d/",
                apiKey, startIndex, endIndex
        );
        System.out.println("요청: " + url);
        JsonNode root = restTemplate.getForObject(url, JsonNode.class);
        JsonNode rows = root.path("ChildCareInfo").path("row");
        System.out.println("row 수: " + rows.size());
        for (JsonNode node : rows) {
            KinderVO vo = KinderVO.builder()
                    .kinderName(node.path("CRNAME").asText())
                    .kinderPhone(node.path("CRTELNO").asText())
                    .kinderPostcode(node.path("ZIPCODE").asText())
                    .kinderAddress(node.path("CRADDR").asText())
                    .kinderCapacity(node.path("CRCHCNT").asInt())
                    .kinderWeekendOpen(node.path("CRSPEC").asText().contains("휴일") ? "Y" : "N")
                    .kinderNightOpen(node.path("CRSPEC").asText().contains("야간") ? "Y" : "N")
                    .build();
            String closed = node.path("CRABLDT").asText();
            String zipcode = node.path("ZIPCODE").asText();
            String phone = node.path("CRTELNO").asText();
            String latitude = node.path("LA").asText();
            String longitude = node.path("LO").asText();
            if (!closed.isBlank()|| zipcode.isBlank()|| phone.isBlank() )
                continue;
            if(kinderMapper.existsByLaAndLo(latitude,longitude)>0) continue;
            System.out.println("중복검사");
            if (!(kinderMapper.existsByNameAndPhone(vo.getKinderName(), vo.getKinderPhone())>0)){
                System.out.println("인서트전");
                kinderMapper.insertKinder(vo);
                LocationVO locationVO = LocationVO.builder().
                        latitude(latitude).
                        longitude(longitude).
                        kinderKey(vo.getKinderKey())
                        .build();
                System.out.println("인서트후");
                locationMapper.registLocation(locationVO);
                System.out.println("위치정보인서트");
            }
        }
        startIndex += pageSize;
    }
}

인프라 & 배포(Infrastructure/ DevOps)

1. AWS

1-1. EC2(Server)

  • 외부에서 EC2에 접속하는 규칙인 인바운드규칙과 EC2에서 나가는 규칙인 아웃바운드규칙이 있으며 기본적으로 모든 포트를 아웃바운드함
  • 인바운드 포트
포트번호 통신규약 텍스트
22 SSH 리눅스 인스턴스 접속에 사용
80 HTTP 비보안 웹서버 포트
443 HTTPS 보안 웹서버 포트
21 FTP 파일 전송 프로토콜
22 SFTP 보안 파일 전송 프로토콜
3389 RDP 윈도우 인스턴스용 포트
  1. 스왑파일 생성
  • dd 명령을 사용해 루트파일 시스템에 스왑 파일 생성
  • bx는 블록의 크기이고, count는 블록 수 , 스왑 파일의 크기는 dd명령의 블록 크기 옵션에 블록 수 옵션을 곱한 값 sudo dd if=/dev/zero of=/swapfile bs=128M count=32
  1. 스왑 파일의 읽기 및 쓰기 권한 업데이트
  • sudo chmod 600 /swapfile
  1. Linux 스왑 영역 설정
  • sudo mkswap /swapfile
  1. 스왑 공간에 스왑 파일 추가
  • sudo swapon / swapfile
  1. 프로시저 확인
  • sudo swapon -s
  1. /etc/fstab 파일을 편집하여 부팅 시 스왑 파일 시작
// 편집기에서 파일을 열어준다.
$ sudo vi /etc/fstab

// 파일 끝에 아래의 내용을 추가하고 파일을 저장한다.(/포함)
/swapfile swap swap defaults 0 0
  1. 메모리 확인명령어로 결과 호가인

free -m

1-2. S3(Simple Storage Service)

// # application.properties
## AWS
cloud.aws.credentials.accessKey=${aws.accessKey}
cloud.aws.credentials.secretKey=${aws.secretKey}
cloud.aws.region.static=${aws.region}
cloud.aws.s3.bucket=${aws.bucket}

// # env파일을 불러오는 세부옵션 설정
public class EnvConfig implements EnvironmentPostProcessor {
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        Dotenv dotenv = Dotenv.configure()
                .directory("./")
                .ignoreIfMissing()
                .load();

        Map<String, Object> dotenvProperties = new HashMap<>();
        dotenvProperties.put("aws.accessKey", dotenv.get("AWS_ACCESS_KEY_ID"));
        dotenvProperties.put("aws.secretKey", dotenv.get("AWS_SECRET_ACCESS_KEY"));
        dotenvProperties.put("aws.region", dotenv.get("AWS_REGION"));
        dotenvProperties.put("aws.bucket", dotenv.get("AWS_S3_BUCKET_NAME"));

        environment.getPropertySources().addFirst(new MapPropertySource("dotenv", dotenvProperties));
    }
}
if (!file.isEmpty()) {
    try {
        String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String originalFileName = file.getOriginalFilename();
        String uuid = UUID.randomUUID().toString();
        String saveName = uuid + "_" + originalFileName;
        String s3FilePath = today + "/" + saveName;
        System.out.println("saveName: " + saveName);
        try (InputStream inputStream = file.getInputStream()) {
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentLength(file.getSize());
            metadata.setContentType(file.getContentType());
            amazonS3.putObject(
                    new PutObjectRequest(bucketName, s3FilePath, inputStream, metadata)
            );
            System.out.println("S3 업로드 성공: " + s3FilePath);
        } catch (Exception e) {
            System.err.println("S3 업로드 실패: " + e.getMessage());
            e.printStackTrace();
            throw e;
        }
        String fileUrl = "https://" + bucketName + ".s3." + amazonS3.getRegionName() + ".amazonaws.com/" + s3FilePath;
        FileVO fileVO = FileVO.builder()
                .fileName(originalFileName)   // 원래의 한글 파일명 (화면 표시용)
                .filePath(fileUrl)            // 실제 접근할 S3 URL (한글 없는 safe 버전)
                .fileUuid(uuid)               // uuid (S3 키 매칭용)
                .postKey(postKey)
                .build();
        System.out.println("********" + fileVO.toString());
        filesMapper.insertFile(fileVO);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

1-3. Aurora and RDS(DB)

2. Github(깃허브)

개발도구

1. IntelliJ IDEA

2. VIsual Studio Code

3. Git

협업 및 문서화

1. Notion

2. Github

프로젝트 상세 내용

0. 공통 기능

1. 기사 서버

1-1. 홈

1-2. 운행정보

2. 유저 서버

2-1. 홈

2-2. 자녀관리

2-3. 게시판

3. 웹소켓 서버