티스토리 뷰

1.    Restful 방식의 서비스 개발

 

-      1 ) 최종 목적

-       : MVC 패턴에서 View는 수행하지 않고, 순수한 데이터( JSON, XML )를 응답으로 제공

 

-      2 ) @RestController 어노테이션을 사용하여 컨트롤을 개발한다.

 

-      3 ) HTTP method

-       : GET, POST 외의 여러가지의 추가적인 전송방식을 활용한다.

 

-      4 ) Request Mapping

-       : C ( Create ) -> PUT

-       : R ( Read ) -> GET

-       : U ( Update ) -> POST

-       : D ( Delete ) -> DELETE

 

-       : URI ( URI 자체가 비즈니스 적인 의미를 가진다. )

-      Ex. 게시물 삭제요청 [ DELETE, /board/bno/77 ]

-      Ex. 게시물 등록요청 [ PUT, /board/new ]


[ 1. 자바 객체를 JSON으로 변환하기 ] (****)

 

[ + 데이터 바인딩 라이브러리 추가 ]

 

더보기

[ + 코드 보기 ]

<?xml version="1.0" encoding="UTF-8"?>
 <!-- 1~2칸 띄우기 -->

<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/maven-v4_0_0.xsd">

   <modelVersion>4.0.0</modelVersion>

   <groupId>org.zerock</groupId>
   <artifactId>chap14</artifactId>
   <version>1.0.0-BUILD-SNAPSHOT</version>

   <!-- 웹에 패키징되는 건 war -->
   <packaging>war</packaging>

   <name>chap14</name>
   <url>http://chap14.example.com</url>
   <description>Spring MVC project - maven</description>


   <!-- 프로젝트 어디서든 가져다 쓸수 있도록 정의 -->
   <properties>
      
      <!-- 자바 버전 -->
      <java-version>11</java-version>
      <!-- <java-home>${env.JAVA_HOME}</java-home> -->

      <!-- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <maven.compiler.source>11</maven.compiler.source>
      <maven.compiler.target>11</maven.compiler.target> -->
      
      <!-- 스프링 프레임 워크 버전 -->
      <org.springframework-version>5.3.21</org.springframework-version>

      <org.aspectj-version>1.9.9.1</org.aspectj-version>
      <org.apache.logging.log4j-version>2.17.2</org.apache.logging.log4j-version>
   </properties>


   <dependencies>

      <!-- =============== Logging =============== -->

      <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-core</artifactId>
         <version>${org.apache.logging.log4j-version}</version>
      </dependency>

      <!-- For Spring framework, HikariCP, DriverSpy logging -->
      <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-slf4j-impl</artifactId>
         <version>${org.apache.logging.log4j-version}</version>
      </dependency>


      <!-- ============= Servlet/JSP ============= -->
      <!-- 이에 따라서 선택해야 하는 톰켓 버전이 달라질 수 있다. -->
         
      <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>javax.servlet-api</artifactId>
         <version>4.0.1</version>
         <scope>provided</scope>
      </dependency>
   
      <dependency>
         <groupId>javax.servlet.jsp</groupId>
         <artifactId>javax.servlet.jsp-api</artifactId>
         <version>2.3.3</version>
         <scope>provided</scope>
      </dependency>

      <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>jstl</artifactId>
         <version>1.2</version>
      </dependency>


      <!-- =============== AspectJ (스프링할 때 rt, weaver 꼭)=============== -->
            
      <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjrt</artifactId> <!-- rt = runtime -->
         <version>${org.aspectj-version}</version>
      </dependency>

      <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjweaver</artifactId>
         <version>${org.aspectj-version}</version>
      </dependency>


      <!-- ================ Spring (밑의 2개는 최소한 꼭) =============== -->

      <!-- spring bean container -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-context</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>

      <!-- spring mvc 구동 -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-webmvc</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>

      <!-- spring test -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-test</artifactId>
         <version>${org.springframework-version}</version>
         <scope>test</scope>
      </dependency>

      <!-- spring-jdbc -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-jdbc</artifactId>
         <version>5.3.21</version>
      </dependency>

      <!-- spring-tx manager -->
      <!-- X/Open XA 규약대로 모든 TX Manager가 구현되어 있다. -->
      <!-- 1. WAS 2. Spring MVC 3. JDBC Driver 4. Database Instance 안에 구현체가 있다. -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-tx</artifactId>
         <version>5.3.21</version>
      </dependency>

      <!-- =============== JDBC =============== -->

      <dependency>
         <groupId>org.bgee.log4jdbc-log4j2</groupId>
         <artifactId>log4jdbc-log4j2-jdbc4</artifactId>
         <version>1.16</version>
      </dependency>
        
      <dependency>
         <groupId>com.oracle.database.jdbc</groupId>
         <artifactId>ojdbc8-production</artifactId>
         <version>21.5.0.0</version>
      
         <type>pom</type>
      
         <exclusions>
            <exclusion>
               <groupId>com.oracle.database.ha</groupId>
               <artifactId>simplefan</artifactId>
            </exclusion>
      
            <exclusion>
               <groupId>com.oracle.database.ha</groupId>
               <artifactId>ons</artifactId>
            </exclusion>
      
            <exclusion>
               <groupId>com.oracle.database.jdbc</groupId>
               <artifactId>rsi</artifactId>
            </exclusion>
      
            <exclusion>
               <groupId>com.oracle.database.jdbc</groupId>
               <artifactId>ucp</artifactId>
            </exclusion>
      
            <exclusion>
               <groupId>com.oracle.database.xml</groupId>
               <artifactId>xdb</artifactId>
            </exclusion>
      
            <exclusion>
               <groupId>com.oracle.database.xml</groupId>
               <artifactId>xmlparserv2</artifactId>
            </exclusion>
         </exclusions>
      </dependency>


      <!-- =============== Testing (버전 4,5 둘다 쓸줄알아야함)=============== -->

      <dependency>
         <groupId>org.junit.jupiter</groupId>
         <artifactId>junit-jupiter-api</artifactId>
         <version>5.8.2</version>
         <scope>test</scope>
      </dependency>

      <!-- ================ DataSource ================= -->

      <!-- HikariCP -->
      <dependency>
         <groupId>com.zaxxer</groupId>
         <artifactId>HikariCP</artifactId>
         <version>5.0.1</version>

         <!-- 이 부분만 뺀다 -->
         <exclusions>
            <exclusion>
               <groupId>org.slf4j</groupId>
               <artifactId>slf4j-api</artifactId>
            </exclusion>
         </exclusions>

      </dependency>

      <!-- ================ MyBatis ================= -->
      <dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.5.10</version>
		</dependency>

      <!-- 마이바티스와 스프링 연동 라이브러리 -->
      <dependency>
         <groupId>org.mybatis</groupId>
         <artifactId>mybatis-spring</artifactId>
         <version>2.0.7</version>
      </dependency>

      <!-- ================ Data-Binding ================= -->

      <!-- 변환 라이브러리 -->

      <!-- (1) 자바객체를 직렬화(Serialize)하여 JSON / XML 로 변환 -->
      <!-- (2) JSON / XML를 역직렬화(De-serialize)하여 자바객체로 변환 -->

      <!-- Java 객체를 JSON으로 변환시켜 준다. -->
      <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
         <version>2.13.3</version>
      </dependency>

      <!-- Java 객체를 XML로 변환해 준다. -->
      <dependency>
         <groupId>com.fasterxml.jackson.dataformat</groupId>
         <artifactId>jackson-dataformat-xml</artifactId>
         <version>2.13.3</version>
      </dependency>

      <!-- Java 객체를 JSON으로 변환 -->
      <!-- 위의 2개는 스프링 프레임워크가 사용하고, gson은 우리가 사용한다. -->
      <dependency>
         <groupId>com.google.code.gson</groupId>
         <artifactId>gson</artifactId>
         <version>2.9.1</version>
      </dependency>

      <!-- ================ File - upload ================= -->

      <dependency>
         <groupId>commons-fileupload</groupId>
         <artifactId>commons-fileupload</artifactId>
         <version>1.4</version>
      </dependency>
    
      <!-- ================ Misc ================= -->

      <dependency>
         <groupId>javax.inject</groupId>
         <artifactId>javax.inject</artifactId>
         <version>1</version>
      </dependency>
   
      <!-- 롬복 -->
      <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         <version>1.18.24</version>
         <scope>provided</scope>
      </dependency>
           
   </dependencies>


    <build>

      <finalName>/</finalName>

        <plugins>

            <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>3.3.2</version>
            </plugin>

            <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-eclipse-plugin</artifactId>
            <version>2.10</version>

                <configuration>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>false</downloadJavadocs>
                </configuration>
            </plugin>

            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <version>3.10.1</version>

               <configuration>
                  <release>${java-version}</release>
               </configuration>
            </plugin>

        </plugins>

    </build>

</project>

 

[ + 자바 객체 Person 객체를 JSON으로 변환 ]

 

더보기

[ + 코드 보기 ]

package org.zerock.myapp.gson;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;

import com.google.gson.Gson;

import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;


@Log4j2
@NoArgsConstructor
public class GsonTests {
	
	@Test
	public void testGson() {
		
		log.trace("testGson() invoked.");
		
		// + JSON 문자열로 변환할 자바객체 Person객체 생성
		// + 변환할 객체의 필드를 모두 사용할 필요는 없기에, 필요 부분만 넣어도 된다.
		Person person = Person.builder().
						age(29).
						name("YOYOYO").
						height(180.2).
						weight(60.3).
						isMale(true).
						hobbies( new String [] { "Movie", "Music", "Sleeping" }).
						build();
		
		assertNotNull(person);
		
		// + 자바 객체 -> JSON으로 serialize(직렬화) 수행
		Gson gson = new Gson();
		String json = gson.toJson(person);
		
		log.info("\t + json : {}", json);
		// + json : {"name":"YOYOYO","age":29,"height":180.2,"weight":60.3,"isMale":true,"hobbies":["Movie","Music","Sleeping"]}
		// + JSON은 위와 같이 필드의 이름을 모두 문자열로 표시한다.
		// + 또한 배열이 {}에서 []로 바뀐다.
		
	} // testGson

} // end class


@Builder
class Person {
	
	private String name;
	private int age;
	private double height;
	private double weight;
	private boolean isMale;
	private String[] hobbies;
	
} // Person

 


[ 2. 자바 객체를 JSON으로 변환하기 2 - list ]

 


[ 3. @RestController + produces - View가 아닌 순수한 데이터를 반환 ] (****)

 

[ 3 - 1. 순수한 데이터로 문자열을 반환 ]

 

[ + html 태그 적용 버전 ]

 

[ 3 - 2. 순수한 데이터로 XML을 반환 ]

 

 

[ 3 - 3. 순수한 데이터로 JSON을 반환 ]

 

 

[ 3 - 4. 순수한 데이터로 XML Or JSON 반환 ]

 

더보기

[ + 코드 보기 ]

package org.zerock.myapp.controller;

import java.util.ArrayList;
import java.util.List;

import org.apache.catalina.tribes.util.Arrays;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zerock.myapp.domain.SampleVO;

import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;


@Log4j2
@NoArgsConstructor

@RestController
@RequestMapping("/sample/")
//+ String / XML / JSON 형식으로 돌려주는 컨트롤러
public class StringXMLJSONController {
	
	// ===================================================================
	// 1. 순수한 데이터로 문자열( String )을 반환하는 헨들러 메소드 작성
	// ===================================================================
	
	// 1 ) 일반 text
	
	@GetMapping(path="/sendText", produces = { "text/plain; charset=utf8" })
	// + produces는 헨들러가 발생시키는 미디어 타입( Content type )을 지정한다.
	// + text/plain; charset=utf8는 utf8로 구성된 평범한 text라는 의미이다.
	public String sendText() {
		
		log.info("sendText() invoked.");
		
		return "안녕하세요!! 반가워요!";
		// + 이제는 더 이상 View의 이름을 반환하지 않고, 순수한 데이터를 반환한다. (***)
		// + http://localhost:8080/sample/sendText로 접속하면 반환된 순수 데이터가 나타난다.
		
	} // sendText
	
	// ===================================================================
	
	// 2 ) html
	
	@GetMapping(path="/sendText2", produces = { "text/html; charset=utf8" })
	// + text/html; charset=utf8로 하면, html 파일로 인식하여 태그를 적용시켜 준다. (**)
	public String sendText2() {
		
		log.info("sendText2() invoked.");
		
		return "<h1>안녕하세요!! 반가워요!</h1>";
		// + 이제는 더 이상 View의 이름을 반환하지 않고, 순수한 데이터를 반환한다. (***)
		// + http://localhost:8080/sample/sendText로 접속하면 반환된 순수 데이터가 나타난다.
		
	} // sendText2
	
	// ===================================================================
	
	// 3 ) MediaType. 적용 ver
	
	@GetMapping(path="/sendText3", produces = { MediaType.TEXT_HTML_VALUE + "; charset=utf8" })
	// + MediaType을 통해 ContentType을 지정해 줄 수는 있으나, charset이 적용되지 않기에 따로 적용해 줘야 하며
	// + MediaType 사용 시에는 반드시 뒤에 VALUE가 있는 버전으로 사용해야 한다.
	public String sendText3() {
		
		log.info("sendText3() invoked.");
		
		return "<h1>안녕하세요!! 반가워요!</h1>";
		// + 이제는 더 이상 View의 이름을 반환하지 않고, 순수한 데이터를 반환한다. (***)
		// + http://localhost:8080/sample/sendText로 접속하면 반환된 순수 데이터가 나타난다.
		
	} // sendText3
	
	// ===================================================================
	// 2. 순수한 데이터로 XML 문서를 반환하는 헨들러 메소드 작성
	// ===================================================================
	
	@GetMapping(path="/sendXML", produces = MediaType.TEXT_XML_VALUE + "; charset=utf8" )
	public SampleVO sendXML() {
		
		log.info("sendXML() invoked.");
		
		SampleVO vo = new SampleVO("JOJOJOJO", 30);
		log.info("\t + vo : {}", vo);
		
		return vo;
		
//		+ URI로 접속하면 아래와 같이 출력된다.
//		<SampleVO>
//			<name>JOJOJOJO</name>
//			<age>30</age>
//		</SampleVO>
		
	} // sendXML
	
	// ===================================================================
	
	// + 배열 버전
	
	@GetMapping(path="/sendXML2", produces = MediaType.TEXT_XML_VALUE + "; charset=utf8")
	public SampleVO[] sendXML2() {
		
		log.info("sendXML2() invoked.");
		
		SampleVO[] vo = { 
					new SampleVO("AAAAAAAA", 10),
					new SampleVO("BBBBBBBB", 20),
					new SampleVO("CCCCCCCC", 30) };
		
		log.info("\t + vo : {}",Arrays.toString(vo));
		
		return vo;
		
//		+ URI로 접속하면 아래와 같이 출력된다.
//		<SampleVOs>
//			<item>
//				<name>AAAAAAAA</name>
//				<age>10</age>
//			</item>
//			<item>
//				<name>BBBBBBBB</name>
//				<age>20</age>
//			</item>
//			<item>
//				<name>CCCCCCCC</name>
//				<age>30</age>
//			</item>
//		</SampleVOs>
		
	} // sendXML2
	
	// ===================================================================
	
	// + list 버전
	
	@GetMapping(path="/sendXML3", produces = MediaType.TEXT_XML_VALUE + "; charset=utf8")
	public List<SampleVO> sendXML3() {
		
		log.info("sendXML3() invoked.");
		
		List <SampleVO> vo = new ArrayList<>();
		
		vo.add(new SampleVO("AAAAAAAA", 50));
		vo.add(new SampleVO("BBBBBBBB", 60));
		vo.add(new SampleVO("CCCCCCCC", 70));
		
		log.info("\t + vo : {}", vo);
		return vo;
		
//		+ URI로 접속하면 아래와 같이 출력된다.
//		<List>
//			<item>
//				<name>AAAAAAAA</name>
//				<age>50</age>
//			</item>
//			<item>
//				<name>BBBBBBBB</name>
//				<age>60</age>
//			</item>
//			<item>
//				<name>CCCCCCCC</name>
//				<age>70</age>
//			</item>
//		</List>
		
	} // sendXML3
	
	// ===================================================================
	// 3. 순수한 데이터로 JSOM 문서를 반환하는 헨들러 메소드
	// ===================================================================
	
	@GetMapping(path="/sendJSON", produces = MediaType.APPLICATION_JSON_VALUE + "; charset=utf8")
	public SampleVO sendJSON() {
		
		log.info("sendJSON() invoked.");
		
		SampleVO vo = new SampleVO("JOJOJOJO", 30);
		log.info("\t + vo : {}", vo);
		
		return vo;
		
//		+ URI로 접속하면 아래와 같이 출력된다.
//		{
//		  "name": "JOJOJOJO",
//		  "age": 30
//		}
		
	} // sendJSON
	
	// ===================================================================
	// 4. 순수한 데이터로 XML or JSON 문서를 반환하는 헨들러 메소드
	// ===================================================================
	// + XML보다는 JSON이 더 가볍고 다루기 편하기에, JSON을 주로 사용한다.
	// + 2개를 동시에 하는 것을 불가능하다.
	// ===================================================================
	
	@GetMapping(
				path="/sendXMLOrJSON", 
				produces = { 
						MediaType.APPLICATION_JSON_VALUE + "; charset=utf8", 
						MediaType.APPLICATION_XML_VALUE + "; charset=utf8" })
	public SampleVO sendXMLOrJSON() {
		
		log.info("sendXMLOrJSON() invoked.");
		
		SampleVO vo = new SampleVO("JOJOJOJO", 30);
		log.info("\t + vo : {}", vo);
		
		return vo;
		
//		+ URI로 접속하면 아래와 같이 출력된다.
		
	} // sendXMLOrJSON
	
	// ===================================================================

} // end class

[ 4. Restful 방식의 컨트롤 헨들러 ] (*****)

 

더보기

[ + 코드 보기 ]

package org.zerock.myapp.controller;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;


@Log4j2
@NoArgsConstructor

@CrossOrigin
// + @CrossOrigin을 붙여 놓으면 동일출처 원칙을 위반해도 데이터를 보내준다.
// + 비동기 호출로도 가능하게 만들어 준다.

@RestController
@RequestMapping("/product/")
public class PathVariableController {
	
	// + Restful 방식의 컨트롤러 헨들러 메소드의 Request Mapping은 아래의 2가지를 잘 조합해야 한다.
	// + (1) Http Method : PUT(C), GET(R), POST(U), DELETE(D) ( 4가지 )
	// + (2) Mapping URI : CRUD 대상 자원을 지정하는 형태로 매핑
	
	// + 아래의 예와 같이, 요청 비지니스 로직을 표현할 수 있는 형태로 전송방식(HTTP method)과
	// + 요청URI(Request URI)를 조합해서 매핑을 표현하는 방식이 바로 "Restful" 방식이다.
	
	// ===================================================================
	// + Request URI 안에 지정된 경로 변수( Path Variable, 기호 : {경로변수명} ) 값 얻는 법
	// + : 스프링에서는 @PathVariable이란 어노테이션을 제공하는데, 이를 통해서 한다.
	// ===================================================================
	
	// ===================================================================
	// Case 1. 신규상품 등록( CREATE ) -> PUT
	// ===================================================================
	// + {}는 경로 구분자이다.
	// + {} 안에는 가변적인 경로가 들어오게 된다.
	// ===================================================================
	
	@PutMapping(path="/{category}/new", produces = MediaType.TEXT_PLAIN_VALUE + "; charset=utf8")
	public String createNewProduct(@PathVariable("category") String productCategory) {
		
		// ===================================================================
		// + Request URI로부터, 경로 변수의 값 얻어내기
		// + : 얻어낸 경로변수 값을 매개변수의 타입에 맞게 형변환까지 해서 넣어준다.
		// + : 주의. 매개변수의 이름은 경로변수명과 직접적인 연관성은 없기에, 자유롭게 작성이 가능하다.
		// ===================================================================
		
		log.trace("createNewProduct() invoked.");
		log.trace("\t + productCategory : {}",productCategory);
		
		return "SUCCESS";
		
	} // createNewProduct
	
	// ===================================================================
	// Case 2. 기존 상품 삭제( DELETE ) -> DELETE
	// ===================================================================
	
	@DeleteMapping(path="/{category}/{id}", produces = MediaType.TEXT_PLAIN_VALUE + "; charset=utf8")
	public String removeProduct(
									@PathVariable("category") String productCategory,
									@PathVariable("id") Integer productId ) {
		
		log.trace("removeProduct() invoked.");
		log.trace("\t + productCategory : {}", productCategory );
		log.trace("\t + productId : {}", productId );
		
		return "SUCCESS";
		
	} // removeProduct
	
	// ===================================================================
	// Case 3. 기존 상품 수정( UPDATE ) -> POST
	// ===================================================================
	
	@PostMapping(path="/{category}/{id}", produces = MediaType.TEXT_PLAIN_VALUE + "; charset=utf8")
	public String updateProduct(@PathVariable("category") String productCategory, @PathVariable("id") Integer productId) {
		
		log.trace("updateProduct() invoked.");
		log.trace("\t + productCategory : {}", productCategory );
		log.trace("\t + productId : {}", productId );
		
		return "SUCCESS";
		
	} // updateProduct
	
	// ===================================================================
	// Case 4. 기존 상품 조회( READ ) -> GET
	// ===================================================================
	
	@GetMapping(path="/{category}/{id}", produces = MediaType.TEXT_PLAIN_VALUE + "; charset=utf8")
	public String readProduct(@PathVariable("category") String productCategory, @PathVariable("id") Integer productId) {
		
		log.trace("readProduct() invoked.");
		log.trace("\t + productCategory : {}", productCategory );
		log.trace("\t + productId : {}", productId );
		
		return "SUCCESS";
		
	} // readProduct
	
} // end class

 

 

 

728x90
댓글
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
최근에 올라온 글
Total
Today
Yesterday
공지사항