반응형

오늘은 얼마 전 개발 중에 사용한 Jquery sort를 포스팅해볼게요.

 

div영역을 직접 정렬을 하려면 코드가 길어지는데 jquery에서 제공하는 sort함수를 사용하면 간단하게 정렬을 할 수 있어요.

코드는 간단하게 작성을 해봤어요.

$("div.process-result").html($('div.process-result div.status-box').sort(sortLiElements));

function sortLiElements(a,b) {

	return parseInt($(b).data('time')) - parseInt($(a).data('time'));

}
            
            
<div class="process-result">
               
	<div class="status-box blue" data-time="20210215171439">
		<p class="title">신청 정보 수정</p>
	</div>
	
 	<div class="status-box blue" data-time="20210215170344">
		<p class="title">신청등록</p>
	</div>
    
</div>

정렬 기준이 되는 값이 필요한데 div태그에 data-time을 사용했어요. sortsortLiElements에서 오름차순 내림차순 정할 수 있어요.

반응형

'개발 > Javascript' 카테고리의 다른 글

Jquery Datepicker 동적생성  (0) 2021.03.03
Jquery each break,continue  (0) 2021.02.17
Javascript delete Json  (0) 2021.01.22
Jquery 이벤트 여러번발생  (0) 2020.11.05
Jquery UI selectmenu trigger  (0) 2020.09.14
반응형

이번에 사용자 패스워드에 암호화 적용을 하여 포스팅하게 되었어요.

패스워드는 보통 단방향을 사용해서 복호화가 안되게 되어야 하고 암호화된 문자들끼리 비교를 하여 패스워드가 맞는지 판별을 하게 돼요. 그 외 암호화, 복호화가 필요한 부분도 있어 양방향 암호화도 만들었어요.

	public static String encryptSha256(String value) {

		MessageDigest md;
		StringBuffer sb = new StringBuffer("");

		try {

			md = MessageDigest.getInstance("SHA-256");
			md.update(value.getBytes());
			byte byteData[] = md.digest();

			for (byte tmpStrByte : byteData) {
				String tmpEncTxt = Integer.toString((tmpStrByte & 0xff) + 0x100, 16).substring(1);

				sb.append(tmpEncTxt);
			}
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return sb.toString();

	}

	public static String encryptAes(String str, String key) throws Exception {

		Cipher cipher = Cipher.getInstance("AES");

		SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");

		cipher.init(Cipher.ENCRYPT_MODE, secretKey);

		byte[] encPassword = cipher.doFinal(str.getBytes("UTF-8"));

		String result = Base64.getEncoder().encodeToString(encPassword);

		return result;
	}

	/*
	* password = AES 방식으로 암호화된 암호문
	* key = 암호화시 사용했던 키워드
	*/
	public static String decryptAes(String str, String key) throws Exception {

		Cipher cipher = Cipher.getInstance("AES");
		SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");

		cipher.init(Cipher.DECRYPT_MODE, secretKey);

		byte[] decPassword = cipher.doFinal(Base64.getDecoder().decode(str));
		String result = new String(decPassword, "UTF-8");

		return result;
	}

단방향 암호화는 SHA-256 방식으로 했어요. 양방향 암호화는 AES 방식으로 사용을 하였는 데 사용되는 key는 값이 16, 24, 32 Byte로 작성이 되어야 해요. 그 외 byte에 안 맞는 키값이 입력이 되면 Invalid AES key length에러가 발생을 하게되므로 반드시 byte길이를 맞춰야해요.

반응형
반응형

 

Server로 JSON값을 전달 중에 몇몇 데이터는 삭제 후 전달을 해야 하는 상황이 나와서 기존하던 방식으로 DELETE를 이용하여 삭제를 하였는데 list안에 있는 json들은 -> [{a:"a"},{empty},{c:"c"}] 같이 나와서 length가 3이 되어 삭제가 정상적으로 안 되는 현상이 발생하여 방법을 찾는 중에 jquery에서 grep라는 함수가 있어서 활용하게 되었어요.

// 	$(formData.reqFundBzobInfo.reqFundFarmlandInfoList).each(function(i,o){

// 		if ( o.delYn == "Y" ) {

// 			delete formData.reqFundBzobInfo.reqFundFarmlandInfoList[i];
// 			return true;

// 		}

// 		$(o.reqfabsFarmlandPredlstInfoList).each(function(j,jo){

// 		if ( jo.delYn == "Y" ){

// 			delete formData.reqFundBzobInfo.reqFundFarmlandInfoList[i].reqfabsFarmlandPredlstInfoList[j];

// 		}

// 		});

// 	});

formData.reqFundBzobInfo.reqFundFarmlandInfoList = $.grep(formData.reqFundBzobInfo.reqFundFarmlandInfoList, function (o,i) {

	if ( o.delYn == "Y" ) {
		
        return false;
        
	} else {

		formData.reqFundBzobInfo.reqFundFarmlandInfoList[i].reqfabsFarmlandPredlstInfoList = $.grep(o.reqfabsFarmlandPredlstInfoList,function (jo,j){

          if ( jo.delYn == "Y" ) {

              return false;

          } else {

              return true;

          }

		});

		return true;
	
	}

});

주석 처리부분이 기존에 사용하던 방법으로 진행하다가 안돼서 아래 방법으로 수정을 하였더니 정상 작동이 되었어요.

삭제가 필요할 경우 대상은 return false로하여 값을 삭제하면 돼요. list가 아니고 그냥 Object면 delete [key] 방식으로 지워도 상관이 없을 거예요. 전 list라 안되어 위 방법을 사용을 했어요.

반응형

'개발 > Javascript' 카테고리의 다른 글

Jquery Datepicker 동적생성  (0) 2021.03.03
Jquery each break,continue  (0) 2021.02.17
Jquery sort 사용하기  (0) 2021.02.15
Jquery 이벤트 여러번발생  (0) 2020.11.05
Jquery UI selectmenu trigger  (0) 2020.09.14
반응형

오늘 테이블 생성중 칼럼에 예약어가 있어 테이블 생성이 안되어 알아보게 되었어요.

CREATE TABLE STATUS_INFO(
		STATUS_ID                     		INT(11)		 NOT NULL COMMENT '현황아이디',
		AREA_CODE                     		CHAR(10)		 NULL  COMMENT '지역코드',
		`DIV`                           		VARCHAR(1000)		 NULL  COMMENT '구분',
		`TYPE`                          		INT(10)		 NULL  COMMENT '유형',
		POPULATION                    		INT(10)		 NULL  COMMENT '인구수',		
		REG_DTM                       		CHAR(14)		 NULL  COMMENT '등록일',
		REG_ID                        		INT(11)		 NULL  COMMENT '등록자',
		MOD_DTM                       		CHAR(14)		 NULL  COMMENT '수정일',
		MOD_ID                        		INT(11)		 NULL  COMMENT '수정자'
) COMMENT='현황정보';

DIV, TYPE 컬럼이 예약어로 등록이 되어있어 일반 칼럼처럼 했더니 에러가 발생하여 ` <- 컬럼명 앞뒤로 묶어서 하면 해결이 돼요.

키보드 특수문자 제일 앞 Tab키 바로 위에 있어요.

보통 예약어는 제가 사용하는 툴에서는 파란색으로 표시가 나오는데 웬만한 툴에서는 이미지처럼 다른 게 표시가 나올 거예요

반응형

'개발 > DB' 카테고리의 다른 글

오라클 연속된 값 찾기  (1) 2024.02.06
MariaDB Sequence(10.3)이상  (0) 2020.11.12
반응형

외부기관 API를 통해서 데이터를 주고받고 하는 부분이 있어 RestTemplate을 사용하는 중에 발생한 에러 포스팅해볼게요.

public ResponseEntity<T2> post(String endpointUrl, T1 body, Class<T2> responseType, HeadersStrategy strategy) {
	HttpEntity<T1> httpEntity = new HttpEntity<>(body, headers(HttpMethod.POST, strategy));

	ResponseEntity<T2> response = restTemplate.postForEntity(endpointUrl, httpEntity, responseType);

	return response;
}

body부분에 VO class를 전달하고 post로 발송하니 서버 쪽에서는 parameter값이 없다 하여 확인을 했어요.

private static boolean romePresent =
			ClassUtils.isPresent("com.rometools.rome.feed.WireFeed",
					RestTemplate.class.getClassLoader());

private static final boolean jaxb2Present =
			ClassUtils.isPresent("javax.xml.bind.Binder",
					RestTemplate.class.getClassLoader());

private static final boolean jackson2Present =
			ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
					RestTemplate.class.getClassLoader()) &&
			ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
					RestTemplate.class.getClassLoader());

private static final boolean jackson2XmlPresent =
			ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper",
					RestTemplate.class.getClassLoader());

private static final boolean gsonPresent =
			ClassUtils.isPresent("com.google.gson.Gson",
					RestTemplate.class.getClassLoader());

실제 RestTemplate 소스 부분이에요. 기본적인 Converter들이에요 일반적인 json방식은 jackson으로 변환이 돼요.

public RestTemplate() {
	this.messageConverters.add(new ByteArrayHttpMessageConverter());
	this.messageConverters.add(new StringHttpMessageConverter());
	this.messageConverters.add(new ResourceHttpMessageConverter());
	this.messageConverters.add(new SourceHttpMessageConverter<Source>());
	this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

	if (romePresent) {
		this.messageConverters.add(new AtomFeedHttpMessageConverter());
		this.messageConverters.add(new RssChannelHttpMessageConverter());
	}

	if (jackson2XmlPresent) {
		this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
	}
	else if (jaxb2Present) {
		this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
	}

	if (jackson2Present) {
		this.messageConverters.add(new MappingJackson2HttpMessageConverter());
	}
	else if (gsonPresent) {
		this.messageConverters.add(new GsonHttpMessageConverter());
	}
}

생성자 소스 부분인데 여기에서 생성할 때 기본적인 Converter를 등록해요.

for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
	if (messageConverter instanceof GenericHttpMessageConverter) {
		GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter;
			if (genericMessageConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
					if (!requestHeaders.isEmpty()) {
						for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
								httpHeaders.put(entry.getKey(), new LinkedList<String>(entry.getValue()));
						}
					}
					if (logger.isDebugEnabled()) {
						if (requestContentType != null) {
							logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
										"\" using [" + messageConverter + "]");
							}
							else {
								logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
							}

						}
						genericMessageConverter.write(
								requestBody, requestBodyType, requestContentType, httpRequest);
						return;
						}
					}
					else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
						if (!requestHeaders.isEmpty()) {
							for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
								httpHeaders.put(entry.getKey(), new LinkedList<String>(entry.getValue()));
							}
						}
						if (logger.isDebugEnabled()) {
							if (requestContentType != null) {
								logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
										"\" using [" + messageConverter + "]");
							}
							else {
								logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
							}

						}
						((HttpMessageConverter<Object>) messageConverter).write(
								requestBody, requestContentType, httpRequest);
						return;
					}
				}

RestTemplate에서 execute실행 전 소스인데 전달받은 body가 해당 converter에 해당되는지 판별하고 값을 write를 하고 있습니다. 저기에 해당이 안되면 body가 없이 전달이 되기 때문에 생성할 때 convert를 전달하거나 기본 내장된 convert를 사용해야 해요. 그래서 사용한 게 Spring에서 제공하는 MultiValueMap이에요. 해당 부분은 MAP처럼 parameter를 put을해서 사용하시면 돼요.

MultiValueMap<String, Object> mmap = new LinkedMultiValueMap<>();

 

 

사용 중에 값은 전달이 되는데 빨간 부분처럼 header에 "multipart/form-data"가 추가되어 API 서버에서 parameter를 못 받는 현상이 발견되어서 확인 중에VO변수 중에 Integer를 사용하는 변수들이 있는데 Integer -> String로 변경 후 실행하니 multipart가 add가 안되었습니다. 왜 Integer를 사용하면 붙는지는 소스를 더 확인을 안 하여 그냥 String을 사용하기로 하였습니다~~~ 

HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setConnectTimeout(HTTP_CONNECT_TIMEOUT);
requestFactory.setReadTimeout(HTTP_READ_TIMEOUT);
farmLandInfoReqRequestTemplatProvider.get().setRequestFactory(requestFactory);

Map<String, Object> map = new HashMap<>();

MultiValueMap<String, Object> mmap = new LinkedMultiValueMap<>();
try {

  map = BeanConverter.toMap(request);
  mmap.setAll(map);

} catch (Exception e) {

}


return farmLandInfoReqRequestTemplatProvider.get().post(url, mmap, BaseReqResponse.class);

최종적으로 body부분을 MultivalueMap으로 변경하여 사용하였습니다.

 

반응형
반응형

List에서 특정값이 중복되는 Model을 제거하고 frlndSn을 값으로 정렬을 해야 하는 상황이 발생하여 나름 중복 제거 및 List 정렬한 소스 포스팅해볼게요.

 

중복 제거가 여러 가지 방법이 있겠지만 전 MAP을 이용했어요. key가 중복되면 마지막 값으로 계속 덮어버리니 마지막 남은 값만 유지가 되어 중복되는 key가 날아가도록 했어요.

List를 담을 때 frlndSn(일련번호)를 주어 순서대로 담았는데 Map을 거치면서 이 순서가 섞이는 거 같아요 List를 출력하니 add 한 순서대로 안 나오네요. 그래서 정렬도 했어요.

  for (ReqFundLivestockInfo reqFundLivestockInfo : reqFundBzobInfo.getReqFundLivestockInfoList()) {

      //같은 Value일경우 덮어버리기
      if (reqFundLivestockInfoMap.get(reqFundLivestockInfo.getValue()) != null) {

          ReqFundLivestockInfo tmpReqFundLivestockInfo = reqFundLivestockInfoMap.get(reqFundLivestockInfo.getValue());

          reqFundLivestockInfoMap.put(reqFundLivestockInfo.getValue(),reqFundLivestockInfo);
          
      } else {
      
          reqFundLivestockInfoMap.put(reqFundLivestockInfo.getValue(),reqFundLivestockInfo);         

      }

  }
	
    //map을 list로 변환
	ArrayList<ReqFundLivestockInfo> sortList = (ArrayList<ReqFundLivestockInfo>) reqFundLivestockInfoMap
							    .values().stream().collect(Collectors.toList());

	//일련번호 asc정렬
	Collections.sort(sortList, (arg0, arg1) -> {
    
		return arg0.getFrlndSn().compareTo(arg1.getFrlndSn());

	});

위 코드 실행결과 Value값이 중복된 Model은 Map에서 가장 마지막 Value만 남아있고 Map을 List로 변환 시 기존 List정렬순서가 섞이므로(왜 index순번이 바뀌는지는 모르겠습니다) sort를 이용하여 정렬을 다시 하였습니다.

예전 배울 때는 Bubble Sort방식으로 했었는데 간단하게 구현이 되네요.

Collections.sort말고 Arrays.sort도 있는데 이거는 배열 정렬에 사용되는거라 Collections.sort을 사용했어요

반응형
반응형

개발중에 UTF-8 사이트에 맞게 하였는데 본인 이클립스에서만 한글이 깨질경우 이클립스ini파일에 UTF-8을 추가해야해요.

전 지금 STS를 사용중이라 SpringToolSuite4 파일을 수정했어요.

-startup
plugins/org.eclipse.equinox.launcher_1.5.800.v20200727-1323.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.1300.v20200819-0940
-product
org.springframework.boot.ide.branding.sts4
--launcher.defaultAction
openFile
-vm
plugins/org.eclipse.justj.openjdk.hotspot.jre.full.win32.x86_64_14.0.2.v20200815-0932/jre/bin
-vmargs

-Dfile.encoding=UTF-8

-Dosgi.requiredJavaVersion=11
-Dosgi.dataAreaRequiresExplicitInit=true
-Xms256m
-Xmx2048m
--add-modules=ALL-SYSTEM
-XX:PermSize=256M
-XX:MaxPermSize=1024M  
-XX:MaxNewSize=512M 
-XX:NewSize=128M
-XX:-UseConcMarkSweepGC 
-Xverify:none 
-javaagent:D:\fabs\sts-4.8.0.RELEASE\lombok.jar

-Dfile.encoding=UTF-8를 -vmargs에 추가를하고 이클립스 재실행후 페이지 동작을하니 한글이 안깨지고 잘 나오고있어요. 혹시 본인 이클립스에서만 한글이 깨질경우 위 설정을 추가하여 해보세요.

반응형

'개발' 카테고리의 다른 글

톰켓 Symbolic link(Windows)  (0) 2020.10.27
티스토리 소스코드 넣기  (0) 2020.08.13
spring security 중복 로그인 방지  (3) 2020.08.12
반응형

오늘은 loop를 돌면서 값 합계를 구해야하는데 th:with를 사용하여 이용을하면 되겠지하여 c:set처럼 사용을 하였습니다.

<th:block th:with="sum=0">
	<th:block th:each="item,status : ${list}">
    	<th:block th:with="sum=sum+item.value">
        	<th:block th:if="${status.last}" th:text="${sum}">
            
            </th:block>
        </th:block>
	</th:block>    
</th:block>

대략 저런 코드였는데 실행을 하였는데 sum이 안되어 이것저것 찾아보게 되었어요 jstl이면 금방되는건데...c:set처럼 block을 벗어나면 값을 호출할수가 없네요

//찾아보니 sum을 할 수 있는 방법이있어서 적용을 하였어요;
#aggregates.sum() 유틸이에요

#aggregates.sum(list.![value]) 이렇게하면 list에 있는 value를 더하여 최종 값을 리턴을 해줘요.

만약 list에서 특정 조건 값만 sum을 하고싶으면 조건을 추가도 가능해요
#aggregates.sum(list.?[type=='user'].![value])

list안에 type이 user인 value값만 sum이되어 최종 값이 리턴이되요.

그러면 위에서 할려던 list안에있는 값을 sum할 수 가 있어요.

Thymeleaf 화면단에서 계산이 힘들어 Server단에서 계산을 할려고했으나 다행이 계산하는 방법이 있어 간단히 해결을 하였어요.

 

Jar 파일안에보니 util class안에 다른 평균도 함수도있네요.

그럼 오늘도 즐 코딩하세요.

반응형

'개발 > Web' 카테고리의 다른 글

카카오톡 인앱브라우저 닫기  (1) 2024.03.25
Thymeleaf Layout구성  (2) 2020.12.06
Thymeleaf spring security 사용하기  (5) 2020.12.02
Thymeleaf 문법  (2) 2020.12.01
반응형

요즘 ThymeThymeleaf을 사용하면 레이아웃을 구성하는 포스팅을 해볼께요.

레이아웃구성

현재 개발중인 화면 레이아웃이에요 PC홈페이지 기준이에요.

 

파일구성

빈페이지,기본페이지,로그인페이지 3가지 버전에 따라 구성을 했어요 지금은 기본페이지 구성을 해볼께요.

default구성이에요
<div class="wrap">
	<!-- header -->
	<header id="header" th:replace="templates/fragments/header :: header">			
	</header>
	<!--// header -->
		
	<!-- breadcrumbs -->
	<div class="breadcrumbs-wrap" th:replace="templates/fragments/breadcrumbs :: breadcrumbs">
	</div>
	<!--// breadcrumbs -->				
				
	<!-- container -->
	<div id="container">
		<div class="inner">
			<!-- section-left -->
			<div class="section-left" th:replace="templates/fragments/left :: left">					
			</div>
			<!--// section-left -->
				
			<!-- contents -->
			<div class="contents" layout:fragment="content">
				contents
			</div>
			<!--// contents -->
		</div>
	</div>
	<!--// container -->
		
	<!-- footer -->
	<div th:replace="templates/fragments/footer :: footer">			
	</div>
	<!--// footer -->
</div>
	
<!-- 공통스크립트 -->
<script th:replace="templates/fragments/script :: script">
</script>

<!-- 페이지스크립트 -->
<th:block layout:fragment="page-script-block">
</th:block>

header(GNB),사이트이동경로표시( breadcrumbs),left(LNB),내용(content),footer.공통스크립트,페이지 스크립트 순서에요

<div id="header" th:fragment="header">

</div>
<div class="section-left" th:fragment="left">

</div>
<div class="breadcrumbs-wrap" th:fragment="breadcrumbs">

</div>
<div class="contents" layout:fragment="content">

</div>
<div id="footer" th:fragment="footer">

</div>

각 구성에 맞게 코딩을해서 구현을 하면되요.

반응형

'개발 > Web' 카테고리의 다른 글

카카오톡 인앱브라우저 닫기  (1) 2024.03.25
Thymeleaf each value sum  (2) 2020.12.15
Thymeleaf spring security 사용하기  (5) 2020.12.02
Thymeleaf 문법  (2) 2020.12.01
반응형

페이지에서 Spring Security를 사용하여 권한별 구조를 바꾸려고 하는 데 사용했던 소스를 적용을 시켰는데 동작을 안 하여 이것저것 살펴봤더니 Config 쪽에 적용이 안 된 부분이 있어 적용을 하였습니다.

<div sec:authorize="hasRole('ROLE_ADMIN')">
  관리자
</div>
@Bean(name = "templateEngine")
public SpringTemplateEngine getTemplateEngine() {
	SpringTemplateEngine templateEngine = new SpringTemplateEngine();

	Set<ITemplateResolver> templatesResolvers = new HashSet<>();
	templatesResolvers.add(getTemplateResolver());
	templateEngine.addDialect(new LayoutDialect());
	templateEngine.setTemplateResolvers(templatesResolvers);
	templateEngine.addDialect(new SpringSecurityDialect());
	return templateEngine;
}

SpringSecurityDialect 클래스를 추가를 해야합니다

Class를 못 찾으면 "Thymeleaf Extras Springsecurity4" Package가 추가안되어있을거에요

 

Thymeleaf Extras Springsecurity4 Maven 링크입니다

 

Maven Repository: org.thymeleaf.extras » thymeleaf-extras-springsecurity4 » 3.0.4.RELEASE

 

mvnrepository.com

현재 프레임워크랑 맞는 버전으로 추가를 하면 SpringSecurityDialect Class가 Import 될 거예요

<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{templates/default-layout}" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

 xmlns:sec="http://www.thymeleaf.org/extras/spring-security" 네임스페이스 추가하세요

그럼 이제 상단에 적용하려고 했던 소스코드가 동작되는 거 확인되실 거예요.

sec태그 말고 다른방법으로는 아래 방법도 있어요
<div th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')') ? true : false}">
	관리자
</div>

ROLE을 여러개 적용할경우
<div th:if="${#authorization.expression('hasAnyRole(''ROLE_ADMIN'',''ROLE_ADMIN2'')') ? true : false}">
	관리자
</div>

그럼 Thymeleaf spring security 태그 적용시켜봤어요

반응형

'개발 > Web' 카테고리의 다른 글

카카오톡 인앱브라우저 닫기  (1) 2024.03.25
Thymeleaf each value sum  (2) 2020.12.15
Thymeleaf Layout구성  (2) 2020.12.06
Thymeleaf 문법  (2) 2020.12.01

+ Recent posts