안녕하세요 워누입니다.

 

학교 과제와 기말고사, 그리고 회사를 퇴사하는 등 이슈가 많아서 한동안 블로그를 방치해버렸습니다..😂😂

이제 종강도 했고, 퇴사도 했으니 블로그를 다시 꾸준하게 작성해볼까 합니다!

그럼 바로 본론으로 진행해볼게요~

.

오늘은 PHP에서 API를 활용하여 다른 서버(URL)로 파일을 전송하는 방법을 포스팅하겠습니다!

 

이전 포스팅 중 PHP로 이미지를 업로드하는 방법이 있었죠?

 

https://seoneu.tistory.com/29

 

[PHP] 이미지 파일 업로드하기!

안녕하세요 워누입니다! 오늘은 PHP로 이미지 파일을 업로드하는 방법을 포스팅하도록 하겠습니다! 거두절미하고 바로 가보도록 하죠~ . 여러분들도, 저도 인터넷에서 사진이나 문서 파일을 웹

seoneu.tistory.com

 

해당 페이지에서 언급했었던 것처럼 다른 서버로 파일을 전송할 때  "multipart/form-data" 설정을 반드시 해주어야 한다고 했었는데요, 이 설정이 무엇을 의미하는지 간략하게라도 알아야 제가 쓴 코드를 보고 이해하거나 직접 작성하실 때 이해를 하기 쉽겠죠?

 

위 페이지에서 포스팅한 내용 중 클라이언트 측의 샘플 코드를 잠깐 보겠습니다!

<!DOCTYPE html>
<html>
<head>
</head>
<title>파일 업로드 테스트</title>
<body>
<form name="reqform" method="post" action="fileUploadResult.php" enctype="multipart/form-data"> 
 <p>이미지 파일 업로드 테스트</p>
 <hr> <br>
 <input type="file" name="imgFile" /><br>
 <input type="submit" value="업로드" />
</form>
</body>
</html>

 

해당 소스에서 form 데이터 내 input type을 "file"로 설정했었던 적이 있었죠?"file" 속성은 말 그대로 파일 업로드를 위해 설정하는 속성인데요, 이를 form 데이터에 담아 서버 측으로 전송하는 원리입니다. 이 때, form 태그의 enctype을 "multipart/form-data"로 지정했었습니다. 

 

"multipart/form-data"로 보내는 이유를 알려면 먼저 HTTP 통신 원리와 Content-type을 알아야 합니다.

 

간략하게 설명을 드리자면 통신 원리는 매우 간단해요!

스펙에 맞게 클라이언트와 서버가 문자로 구성된 데이터를 송수신하는 것이라고 생각해주시면 됩니다. 파일을 전송한다고 해서 jpg 파일이나 txt 파일 자체가 전송되는게 아니라 파일 역시도 문자의 형태로 전송되는 것이고, 이러한 문자의 형태를 스펙에 맞추어 서버에 송신하는 것 뿐이거든요.  이미지, 텍스트, 동영상 파일 모두 문자열로 이루어져 있는데 확인하고 싶으신 분들은 아무 이미지 파일이나 동영상 파일 등을 메모장 등 텍스트 편집기로 열어보세요!

 

Content-Type은 Body에 전송되는 메세지(데이터)의 타입을 정의하는 속성으로 HTTP 요청 Header에서 정의합니다. 이 중에서 파일을 송신할 때 사용하는 속성값이 바로 "multipart/form-data" 형식인 것이지요. 

 

관련해서 참고하기 좋은 블로그가 있어서 공유해봅니다! 

https://velog.io/@shin6403/HTTP-multipartform-data-%EB%9E%80

 

상세 내용은 위에서 보시고 핵심인 PHP 코드를 같이 볼까요?

<?php
header("Content-Type:text/html; charset=utf-8;"); 

// 요청 데이터(API 요구 사항에 맞게 파라미터 설정)
$value_1 = "value";				// key_1에 대한 value 
$value_2 = "value";				// key_2에 대한 value


// 파일 절대 경로 설정 (예시)
$fileDir = "C:/Bitnami/wampstack-7.2.14-0/apache2/htdocs/img/test.jpg";

// 요청자 검증을 위한 암호화 데이터(수정 금지).
$userId = "ID";
$userPwd = "Password";
$EncryptBase64 = base64_encode($userId.":".$userPwd);

// 요청 데이터를 배열 형식으로 최초 set.
$data = array(
	'key_1' => $value_1,
	'key_2' => $value_2,
	'fileDir' => $fileDir,	// 파일경로
	'fileContents' => file_get_contents($fileDir)	// 파일 내용
);	

// 배열 형식의 데이터를 multipart/form-data 형식으로 변환
$postData = setMultipartFormData($data);

// 요청 API Header 정보(Api-Key 값은 전달받은 값 그대로 사용.)
$headers = array(
	'Content-Type: multipart/form-data;boundary=^*******^',
	'Content-Length: '.strlen($postData), 
	'Authorization: Basic '.$EncryptBase64
);

// 요청 URL 
$postUrl = "https://";

// 응답 파라미터 변수 선언 및 초기화
$response = "";

// API 호출 및 응답 데이터 저장
$response = reqPost($postData, $postUrl, $headers); 
	
// 응답 데이터 print
echo "Response : " . $response;

// api 요청
function apiCall($data, $url, Array $headers){
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_HEADER, true);
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);	
	curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);		//connection timeout 30
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);	
	curl_setopt($ch, CURLOPT_POST, true);			// API 호출 시 method를 post로 지정
	curl_setopt($ch, CURLOPT_POSTFIELDS, $data);	// Data 
	$response = curl_exec($ch);
	curl_close($ch);	 
	return $response;
}

// multipart/form-data 형식으로 변환하는 함수
function setMultipartFormData($arrayData){
	
	$fileData = $arrayData['fileContents'];
	$boundary = "^*******^";
	$data = "";
	unset($arrayData['fileContents']);

	foreach ($arrayData as $name => $content) {
		$data .= '--' . $boundary . '\r\n'
		. 'Content-Disposition: form-data; name="' . $name . '\"\r\n\r\n'
        . $content . '\r\n';
	}
		
	// 파일 데이터 저장
	$data .= '--' . $boundary . '\r\n'
		. 'Content-Disposition: form-data; name="file"; filename="' . $arrayData['fileDir'] . '"' . '\r\n'
		. 'Content-Type:application/octet-stream' . '\r\n\r\n';
	$data .= $fileData . '\r\n';
	$data .= "--" . $boundary . '--\r\n';

	return $data;
   }
?>

 

** 주의사항 : 요청 필드는 제가 임의로 작성해서 Key_1, Key_2 등으로 만든 것이지만 실제로는 API 명세서에서 요구하는 필드를 입력하여야 합니다. 일반적으로 파일 절대 경로나 파일명을 요구하므로 fileDir이라는 필드를 만들었지만 이는 제가 임의로 작성한 코드이므로 API 명세에 따라 수정이 필요합니다. 

또한 예시 코드로 작성하다보니 인증 방식 역시 HTTP 기본 인증 방식인 Basic Authorization을 선택하여 만들었습니다. 실제 코드를 작성하실 때 API 명세에 별도 암호화 방식을 요구할 수 있으니 주의하셔야 합니다.

 

이렇게 예시 코드를 작성해 보았는데요,

위에서 언급했던 "multipart/form-data" 파라미터는 $headers에 담아 HTTP header 정보에 담았습니다!

 

이중 가장 자세히 보아야 할 부분은 제가 임의로 만든 setMultipartFormData 함수입니다!

 

배열로 세팅한 $data를 매개변수로 받아 key-value 데이터와 파일 내용을 분리해서 다시 세팅하는 형태로 구성되어 있는데요, 굳이 이러한 과정을 거치는 이유는 multipart/form-data 형식을 준수하기 위함입니다!

 

우선 첫째로 $boundary 변수는 header에 Content-type=multipart/form-data 지정 시 함께 지정한 boundary와 같은 값을 가져야 합니다. 이유는 boundary가 HTTP 요청 Body 데이터 중 파일 데이터를 구분하기 위한 역할을 가지기 때문입니다. 

 

둘째로 $boundary 양 옆으로 "--" 기호와 "\r\n" 이스케이프 시퀀스를 넣어 놓았는데요,

먼저 "--" 기호는 body의 끝을 알리는 구분자이며 "\r\n" 이스케이프 시퀀스는 Header와 Body, Body 데이터 간의 구분을 위해 넣었다고 생각하시면 됩니다!

 

...?

이해가 되지 않는 부분들이 생기실 거에요..!

여기선 요청 Body 데이터만 세팅하는데 왜 또 header/body를 구분하느냐 말이죠??

 

이유는 body의 내용을 보시면 이해되실 거에요!!

body 데이터에서 Content-Disposition을 지정해주었죠? 이는 HTTP 응답 Header의 한 종류로 브라우저에 Content가 웹페이지로 나타낼지, 다운로드될지 정하는 속성이에요. 즉, $headers에 담은 HTTP 요청 Header 정보가 아닌 응답 Header 정보와 구분하기 위해 "\r\n"을 두번 써서 구분하는 것이랍니다 ㅎㅎ 

Body 데이터 간 구분은 "\r\n"을 한번 써서 구분합니다!

 

이외 자세한 HTTP 통신 규약은 아래 사이트를 참고해주세요!

https://www.donnywals.com/uploading-images-and-forms-to-a-server-using-urlsession/

 

추가적으로 응답 헤더 정보인

Content-Disposition, application/octet-stream에 대한 자세한 내용은 각각 아래 페이지를 참고해주세요!

 

Content-Disposition :

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition

 

Content-Disposition - HTTP | MDN

In a regular HTTP response, the Content-Disposition response header is a header indicating if the content is expected to be displayed inline in the browser, that is, as a Web page or as part of a Web page, or as an attachment, that is downloaded and saved

developer.mozilla.org

Content-Type: application/octet-stream : https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types

 

MIME 타입의 전체 목록 - HTTP | MDN

다음은 일반적인 확장자로 정렬된, 문서 타입과 관련된 MIME 타입의 포괄적인 목록입니다.

developer.mozilla.org

 

참고해야할 사이트가 많지만 찾아보면 분명 도움이 되실 내용들입니다!

.

제가 위에 작성한 코드들은 자바로 작성되어 있던 샘플과 위 페이지를 참고하여 만든 것으로,

필요하신 분들은 가져가서 작성해주셔도 무방합니다만 그냥 복사-붙여넣기하는 방식은 본인에게 제일 좋지 않은 개발습관이에요!

몇몇 분들께서 제 코드를 가져다가 본인이 작성한 코드라고 말하다가 신고되었다고 댓글로 알려주신 사례가 꽤나 적지 않네요..물론 바로 지워주시긴 하셨지만..

 

저 역시도 초급 개발자이다보니 많은 웹 페이지와 서적들을 보고 공부하느라 내용이 틀릴 수도 있으니 만약 제가 작성한 내용을 보시고 틀린 부분이 있다면 지적해주세요 ㅎㅎ

 

오늘 포스팅은 여기서 마치도록 할게요!

긴글 읽어 주셔서 감사합니다~~😃😃😃

오랜만에 찾아뵙는 블로거 워누입니다.

 

오늘 포스팅은 심심해서 만들어본 자바 달력 프로그램입니다.

생각나는 대로 만들어본거라 조금은 어설플 수 있지만 도움이 될 것 같으신 분들은 참고하셔도 좋을 것 같네요! ㅎㅎ

 

 

[코드 참조]

package standard;

import java.util.Scanner;

public class Calendar {

 public static void main(String[] args) {
      // TODO Auto-generated method stub
  
      Scanner sc = new Scanner(System.in);
  
      /*
       * monthSet은 각 달에 대한 날짜들을 저장한 배열,
       * year은 연도, month는 달, week는 요일, day는 일, i는 반복문을 사용하기 위한 변수.
       */
      int year=0, month=0, week=0, day=0, i=0; 
      int monthSet[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  
  
      while(true) {
       System.out.println("원하는 연도를 입력하세요. (0을 누르면 프로그램을 종료합니다.)");
       year = sc.nextInt();
       if(year==0) {
        System.exit(0);
        break;
       }
       System.out.println("원하는 달을 입력하세요.");
       month = sc.nextInt();
       if(month<1 || month>12) {
        System.out.println("1월에서 12월 사이의 달을 입력해주세요.");
        return;
       }
       System.out.println("");
   
       if(year%4 == 0 && year%100 != 0 || year%400 == 0) // 윤년의 조건.
        monthSet[1] = 29; // 윤년이면 2월 (monthSet[1])은 29일,
       else
        monthSet[1] = 28; // 윤년이 아니면 2월은 28일이다.
   
       // 01년 1월 1일부터 계산.
       day = (year-1)*365 + (year-1)/4 - (year-1)/100 + (year-1)/400;
       for(i=0;i<month-1;i++) {
        day += monthSet[i];  
       }
       week=day%7; // 구하고자 하는 달의 시작일(1일)의 요일을 구함.
   
       System.out.printf("%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", "월", "화", "수", "목", "금", "토", "일");   
       for(i=0;i<week;i++) {
        System.out.print("\t"); // 1일이 시작하기 전 공백을 위한 반복문.
       }
       for(i=1;i<=monthSet[month-1];i++) {
        System.out.printf("%d\t", i); // i를 입력한 달의 일수만큼 찍음.
        week++;   
        if(week%7 == 0)        // 요일이 일요일이 지나면
         System.out.println(); // 줄을 바꿈.
    
       }
       if(week%7 != 0) {
        System.out.println();
       }
      }
  
 }

}

 

코드는 생각보다는 간단하더라구요.

윤년을 반영하는 부분이 초보자분들에게는 어려울 것 같습니다.ㅎㅎ 아래는 결과화면인데요, 코드를 작성한 날짜와, 가장 최근 윤달이었던 달, 그리고 제가 태어난 생년월일을 맞추어보았습니다. 0을 누르면 종료하게 되어 있습니다!

 

 

.

[결과 화면]

 

.

혹시 과제 때 필요해서 들어오신 분들이라면 참고하셔도 좋습니다만, Ctrl+C,V 가 아니라 직접 코딩해보는 것을 추천합니다! 그래야 실력이 늘겠죠?

.

 

시간이 늦었으니 저는 이만 자러갈게요~ 안녕! 

블로거 워누입니다!

최근에는 너무 SDN만 건드린거 같아요! 이제 자바와 HTML도 다시 업데이트 해야 겠습니다! (아무래도 SDN이 프로젝트이다 보니 힘들긴 합니다..)

그럼 바로 본론으로 넘어갈게요!

.

이번 포스팅에서는 자바 Swing에서 지원해주는 JComboBox와 ImageIcon을 활용해서 원하는 이미지를 불러오고자 합니다! 코드는 주석을 합쳐도 100줄이 안넘어가니 부담없이 봅시다!

 

<소스 코드>

import java.awt.*;    // java.awt의 모든 라이브러리 클래스들을 제공받습니다.
import java.awt.event.*; // java.awt.event의 모든 라이브러리 클래스들을 제공받습니다.
import javax.swing.*;  // javax.swing의 모든 라이브러리 클래스들을 제공받습니다.

/*
 * ComboActionEx 클래스를 선언합니다.
 * JFrame을 상속받습니다.
 */

public class ComboActionEx extends JFrame{

 String[] fruits = {"apple", "banana", "mango"}; // String 타입 배열 friuts를 선언합니다.
 // 안의 원소들은 apple, banana, mango입니다. (파일 명이기도 합니다.)
 
 /*
  *  Swing에서 지원해주는 ImageIcon 클래스입니다.
  *  이미지 경로를 저장한 후 화면에 출력하는 역할을 합니다.
  *  ImageIcon 객체를 생성할 때 이름은 일반적으로 이미지 경로의 상위폴더명과 동일하게 합니다.
  *  즉, images라는 폴더는 apple, banana, mango 파일을 가지고 있어야 합니다.
  */
 ImageIcon[] images = { // ImageIcon 클래스 객체 images를 배열로 선언합니다.
   new ImageIcon("images/apple.jpg"), // [0]
   new ImageIcon("images/banana.jpg"), // [1]
   new ImageIcon("images/mango.jpg"), // [2]
 };
 
 // JLabel 객체 imgLabel을 생성합니다. 초기값은 images[0]입니다.
 JLabel imgLabel = new JLabel(images[0]);

ComboActionEx(){ //ComboActionEx를 재정의합니다.
  
  setTitle("ComboBox 활용"); // 창의 이름은 ComboBox로 저장합니다.
  /*
   * Container는 Swing에서 제공하는 라이브러리 클래스입니다.
   * 자바에서 창의 역할을 담당하며 Component들이 올려질 영역입니다.
   * contentPane은 화면에 출력될 Component들이 부착될 공간입니다.
   */
  
  Container c = getContentPane();
  
  // Container 클래스 객체 C를 생성 후 contentPane을 가져옵니다.  
  c.setLayout(new FlowLayout()); // c의 레이아웃를 FlowLayout 클래스를 생성하여 잡아줍니다.
  JComboBox combo = new JComboBox(fruits); // ComboBox 클래스 객체 combo를 선언합니다. fruits[0]을 초기값으로 합니다.
  c.add(combo); // 컨테이너(창)에 combo를 더합니다.
  c.add(imgLabel); // 컨테이너에 imgLabel을 더합니다.
  
  /*
   * ActionListener는 보통 버튼에 이벤트가 발생하면 실행됩니다.
   * 아래 코드를 통해 콤보박스에서 선택한 것에 대한
   * 이벤트 처리입니다.
   */
  combo.addActionListener(new ActionListener(){ // combo에 ActionListner를 설정합니다.
   public void actionPerformed(ActionEvent e){ // actionPerformed 메서드를 통해 이벤트 처리에 대한 동작을 구현합니다.
    JComboBox cb = (JComboBox) e.getSource(); // 동작이 일어날 소스를 JComboBox 형태로 받습니다.
    int index = cb.getSelectedIndex(); // int 타입 index를 선언하고 콤보박스에서 선택된 번호 값으로 저장합니다.
    imgLabel.setIcon(images[index]); // imgLabel에 image[index]를 불러옵니다.
   } // end actionPerformed()
  }); // end combo.addActionListener
  
  setBounds(50, 50, 400, 400); // 윈도우 상에서 위치를 잡아 준 후(가로 50, 세로 50) 창의 크기를 설정합니다.(가로 400, 세로 400)
  setVisible(true); // 윈도우에서 클래스를 구현한 창이 보이게끔 합니다.
 } // end ComboActionEx()
 
 public static void main(String[] args) { // 프로그램의 실행지점인 main 메서드입니다.
  // TODO Auto-generated method stub
  new ComboActionEx(); // new 연산자를 이용하여 ComboActionEx()를 실행합니다.
 } // end main

} // end class

 

 

이클립스에서 보니 70줄도 안되네요ㅎㅎ 주석도 한줄마다 달아서 쉽게 이해하실 수 있을 겁니다!

.

여기서 주의하실 점!

(아래 그림을 참고하세요!!!)

.

 

.

이렇게 이미지 경로를 잡아주어야 합니다!

(이클립스 내에서 폴더 생성을 하고 이미지를 Ctrl+C / Ctrl + V로 밀어 넣으셔도 됩니다.)

이걸 안해주시면 당연히 이미지를 불러 올 수 없으므로 에러가 발생하겠죠?

자, 그럼 어떻게 빌드가 되었는지 한번 볼까요?

.

.

와아~ 이미지가 보여요!

이미지 중에 제일 작은 망고 이미지에 맞추다 보니 사과 사진이 좀 잘렸네요..ㅜ 나중에 업그레이드 시켜야겠습니다ㅎㅎ 그럼 다른 항목들은 괜찮은지 콤보박스를 이용해 목록을 바꿔보겠습니다.

.

.

오오.. 망고는 잘 맞고.. 바나나는 잘렸네요!ㅜㅜ 괜찮습니다!

지금은 GUI를 이용해 이미지를 가지고 놀아봤다는 거 자체로 만족할려고 합니다! 그리고 조금씩 실력을 키워서 저런 것들을 능숙하게 다룰 수 있도록 노력해봐야 겠습니다!

.

코드는 짧은데 포스팅이 길어지면 읽기도 지루하시겠죠?ㅋㅋ

(쓰는 저도 살짝 지루해질뻔...ㅎ)

그러므로 저는 이만 포스팅을 마무리하겠습니다. 다음 포스팅에서 만나요~

안녕~~~

+ Recent posts