I/O

Byte Vs Character 어떤것으로 읽을것이냐

Stream Vs RandomAccess 어떻게 읽을것이냐

data Vs metadata 핸들링은 어떻게


Stream 두가지 특징.

1. Sequence : 데이터가 순서대로 들어옴.

2. Concurency X : 동시성을 가지지 않는다. 읽어오면 읽어오기만 할 수 있고 그외는 실행하지않는다.


byte는 InputStream and OutputStream로 처리

character는 Reader and Writer 로 처리

-----------------------------------------

byte

import java.io.IOException;

import java.io.InputStream;


public class InputDemo {

public static void main(String[] args) {

System.out.println("한개의 글자를 입력하세요 : ");

InputStream is = System.in;

int su = 0;

try{

su = is.read();  //하나를 읽어서 su에 넣어줌.

System.out.println("su = " + su);

}catch(IOException ex){}

}

}

출력:

한개의 글자를 입력하세요 : 

a

su = 97

//아스키코드 값로 받는다.

System.out.println("su = " + (char)su); 

//위와 같이 쓰면 한개의 글자를 입력 받으면, 입력받은 글자를 출력한다.

------------

import java.io.IOException;

import java.io.InputStream;


public class InputDemo {

public static void main(String[] args) {

System.out.println("좋아하는 계절 입력하세요 : ");

InputStream is = System.in;

int su = 0;

String str = "";

try{

while(true){

su = is.read();

if(su<0 || (char)su == '\n') break;

str += (char)su;

}

System.out.println("str = " + str);

}catch(IOException ex){}

}

}

출력:

좋아하는 계절 입력하세요 : 

spring

str = spring

//위의 두개의 코드는 한글을 입력받지 못한다. 입력하면 깨지는 현상이 발생하거나 전혀다른값을 얻는다.

--------------------------------------

import java.io.IOException;

import java.io.InputStream;


public class InputDemo1 {

public static void main(String[] args) {

System.out.print("좋아하시는 계절을 입력하세요 : ");

InputStream is = System.in;

int count = 0;

byte [] buffer = new byte[10];

String str = "";

try{

while((count = is.read(buffer)) >= 0){

str += new String(buffer, 0, count);  //(byte[] bytes, 얼마부터, 몇개)

}

System.out.println("str = " + str);

}catch(IOException ex){

System.out.println(ex.getMessage());

}

}

}

//한글입력이 가능한 코드. 그런데 잘 안나오넹... 

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

파일처리

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.util.Scanner;


public class FileInputDemo {

public static void main(String[] args) {

FileInputDemo fid = new FileInputDemo();

String path = fid.getPath();

byte [] buffer = new byte[512];

int count = 0;

FileInputStream fis = null;

String str = "";

try{

fis = new FileInputStream(path);   //open

while ((count = fis.read(buffer)) > 0 ){

str += new String(buffer, 0, count);

}

System.out.println(str);

}catch(FileNotFoundException ex){

System.out.println("File Not Found");

}catch(IOException ex){

System.out.println((ex.getMessage()));

}finally{

try{  //파일을 닫을때도 exception 처리를 해줘야함.

fis.close();                                  //close

}catch(IOException ex){}

}

}

String getPath(){

Scanner scan = new Scanner(System.in);

System.out.print("읽고 싶은 파일 경로 : ");

return scan.next();

}

}

출력:

읽고 싶은 파일 경로 : /home/mino/sungjuk_utf8.dat

1101     한송이     78     87     83    78

1102     정다워     88     83     57    98

1103     그리운     76     56     87    78

1104     고아라     83     57     88    73

1105     사랑해     87     87     53    55

1106     튼튼이     98     97     93    88

1107     한아름     68     67     83    89

1108     더크게     98     67     93    78

1109     더높이     88     99     53    88

1110     아리랑     68     79     63    66

1111     한산섬     98     89     73    78

1112     하나로     89     97     78    88

----------------------------------------------------------

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.util.Scanner;


public class FileInputDemo {

public static void main(String[] args) {

FileInputDemo fid = new FileInputDemo();

String path = fid.getPath();

//byte [] buffer = new byte[512];

int su = 0;

FileInputStream fis = null;

//String str = "";

try{

fis = new FileInputStream(path);   //open

while(true) {

su = fis.read();

if(su < 0) break;

System.out.print((char)su);

}

}catch(FileNotFoundException ex){

System.out.println("File Not Found");

}catch(IOException ex){

System.out.println(ex.getMessage());

}finally{

try{

fis.close();                                  //close

}catch(IOException ex){}

}

}

String getPath(){

Scanner scan = new Scanner(System.in);

System.out.print("읽고 싶은 파일 경로 : ");

return scan.next();

}

}

출력:

읽고 싶은 파일 경로 : src/FileInputDemo.java

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.util.Scanner;


public class FileInputDemo {

~~~~~이어짐

------------------------위코드를 A~Z, a~z의 개수를 세는 프로그램

//영어 알파벳 카운트

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.util.Scanner;


public class FileInputDemo1 {

public static void main(String[] args) {

FileInputDemo1 fid = new FileInputDemo1();

String path = fid.getPath();

int [] array = new int[52];

int su = 0;

FileInputStream fis = null;

try{

fis = new FileInputStream(path);   //open

while(true) {

su = fis.read();

if(su < 0) break;

if(su >= 65 && su <= 90){   //알파벳 대문자일 경우   

++array[su - 65];

}else if(su >= 97 && su <= 122){  //알파벳 소문자일 경우

++array[su - 71];

}

}

fid.output(array);

}catch(FileNotFoundException ex){

System.out.println("File Not Found");

}catch(IOException ex){

System.out.println(ex.getMessage());

}finally{

try{

fis.close();                                  //close

}catch(IOException ex){}

}

}

void output(int [] array){

int count = 0;

for(int i = 0 ; i < 26 ; i++){

System.out.printf("%c = %d\t", (i +65), array[i]);

count++;

if(count % 5 == 0) System.out.println();

}

System.out.println();

count = 0;

for(int i = 26 ; i < array.length ; i++){

System.out.printf("%c = %d\t", (i +71), array[i]);

count++;

if(count % 5 == 0) System.out.println();

}

}

String getPath(){

Scanner scan = new Scanner(System.in);

System.out.print("읽고 싶은 파일 경로 : ");

return scan.next();

}

}

출력:

읽고 싶은 파일 경로 : src/FileInputDemo.java

A = 0 B = 0 C = 0 D = 3 E = 5

F = 12 G = 0 H = 0 I = 9 J = 0

K = 0 L = 0 M = 1 N = 3 O = 3

P = 2 Q = 0 R = 0 S = 15 T = 0

U = 0 V = 0 W = 0 X = 0 Y = 0

Z = 0

a = 32 b = 6 c = 23 d = 7 e = 53

f = 10 g = 9 h = 9 i = 46 j = 4

k = 1 l = 22 m = 16 n = 44 o = 29

p = 24 q = 0 r = 29 s = 24 t = 60

u = 24 v = 5 w = 5 x = 10 y = 10

z = 0

---------------------------------------------------------

import java.io.BufferedInputStream;

import java.io.BufferedOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;


//java BufferedDemo  원본  타겟

public class BufferedDemo {

public static void main(String[] args) {

BufferedInputStream bis = null;

BufferedOutputStream bos = null;

if(args.length != 2){  //두개가 들어와야하는데 파라미터가 두개가아니면

System.out.println("잘못된 명령어입니다.");

System.out.println("Usage : java BufferedDemo  source  target");

System.exit(-1);

}

int su = 0, count = 0;

try {

bis = new BufferedInputStream(new FileInputStream(new File(args[0].trim())));

bos = new BufferedOutputStream(new FileOutputStream(new File(args[1].trim())));

while(bis.read()>=0){ 

bos.write(su);  //하나읽고 하나보내주는 방식

count++;

}

System.out.printf("%d bytes copy successfully.", count);

} catch (IOException e) {

System.out.println(e.getMessage());

} finally{

try{

if(bis != null) bis.close();

if(bos != null) bos.close();

}catch (IOException e) {}

}

}

}

//출력은 터미널창에가서 아래와 같이 쓰면

mino@Ubuntu-22:~/JavaRoom/0331/bin$ java BufferedDemo ../src/BufferedDemo.java /home/mino/Downloads/test.java

1161 bytes copy successfully.mino@Ubuntu-22:~/JavaRoom/0331/bin$ ^C

실행을 하고 그뒤에 복사할내용 그리고 저장될위치를 적어주면, 저장된위치를 가보면 test.java파일이 생성되어있다. 그 내용은 BufferedDemo.java의 내용과 같다.

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

스트림을 리더로, 바이트를 캐릭터로 바꿔주는

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;


//Bridge Class : InputStream --> Reader

public class InputStreamReaderDemo {

public static void main(String[] args) throws IOException{  //위에서는 try,catch를 썼지만 익숙해졌으니 편하게 던지자.

System.out.print("당신은 어느 계절을 좋아하시나요 ? ");

//InputStream is = System.in;   //byte

BufferedReader br = null;

br = new BufferedReader(new InputStreamReader(System.in));  //위위주석라인을 여기에 합친 것임.

String season = br.readLine(); //BufferedReader로 입력받으면 한줄을 입력받는 명령어가 있다.

System.out.println("season = "  + season);

br.close();

}

}

출력:

당신은 어느 계절을 좋아하시나요 ? 가을

season = 가을

--------------------------------------------------------

import java.io.File;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.OutputStreamWriter;


//char --> byte : Writer --> OutputStream

public class OutputStreamWriterDemo {

public static void main(String[] args) throws IOException{

int su = 5;

double avg = 89.5;

String name = "한송이";

char grade = 'B';

boolean isStop = true;

OutputStreamWriter osw = null;

osw = new OutputStreamWriter(new FileOutputStream(new File("./result.dat")));

osw.write(name + " : " + avg + "," + isStop + "," + grade);

osw.close();

}

}

출력은 프로젝트를 refresh하면 생김.

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

import java.io.BufferedWriter;

import java.io.File;

import java.io.FileWriter;

import java.io.IOException;

import java.io.PrintWriter;



public class PrintWriterDemo {

public static void main(String[] args) throws IOException {

String line = "1101     한송이     78     87     83    78";

//PrintStream ps = null;

//BufferedWriter bw = null;

PrintWriter pw = null;  //

pw = new PrintWriter(new BufferedWriter(new FileWriter(new File("./result.txt"))));

pw.println(line);

pw.flush();  //buffer를하면 항상 flush를 써줘야함.

pw.close();

}

}

출력은 프로젝트를 refresh하면 생김.

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

NotePad 만들기

import java.awt.BorderLayout;

import java.awt.Container;

import java.awt.Font;

import java.awt.GraphicsEnvironment;

import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.awt.event.InputEvent;

import java.awt.event.ItemEvent;

import java.awt.event.ItemListener;

import java.awt.event.KeyAdapter;

import java.awt.event.KeyEvent;

import java.awt.event.WindowAdapter;

import java.awt.event.WindowEvent;

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.File;

import java.io.FileReader;

import java.io.FileWriter;

import java.io.IOException;

import java.util.Vector;


import javax.swing.DefaultComboBoxModel;

import javax.swing.ImageIcon;

import javax.swing.JButton;

import javax.swing.JComboBox;

import javax.swing.JFileChooser;

import javax.swing.JFrame;

import javax.swing.JMenu;

import javax.swing.JMenuBar;

import javax.swing.JMenuItem;

import javax.swing.JOptionPane;

import javax.swing.JScrollPane;

import javax.swing.JTextArea;

import javax.swing.JToolBar;

import javax.swing.KeyStroke;

import javax.swing.filechooser.FileNameExtensionFilter;



public class Notepad extends KeyAdapter implements ActionListener, ItemListener{

private JFrame f;

private Container con;

private JToolBar toolbar;

private JButton btnNew, btnOpen, btnSave;

private JScrollPane pane;

private JTextArea ta;

private int count;

private JComboBox<String> combo, combo1 ;

private Font font;

private JMenuBar mb;

private JMenu mFile, mHelp;

private JMenuItem miOpen, miSave, miNew, miExit;

private Notepad(){

this.f = new JFrame("Notepad");

this.con = this.f.getContentPane();

miNew = new JMenuItem("New", new ImageIcon("images/new.gif"));  //메뉴아이템에서 글씨와 그림을 둘다 넣음.

miNew.setMnemonic(KeyEvent.VK_N);  //단축키를 n으로 줌.

miNew.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, 

       InputEvent.CTRL_MASK)); //단축키를 n으로주고 ctrl + n 으로 누르면 실행되게

miNew.addActionListener(this);

mFile = new JMenu("File");  

miOpen = new JMenuItem("Open");  //메뉴아이템에서 글씨만 주는경우

miOpen.setMnemonic(KeyEvent.VK_O);

miOpen.addActionListener(this);

miSave = new JMenuItem(new ImageIcon("images/save.gif"));  //메뉴아이템에서 그림만 주는경우. 그림에는 단축키 불가능.

mFile = new JMenu("File");

miOpen = new JMenuItem("Open");

miOpen.setMnemonic(KeyEvent.VK_O);

miOpen.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, 

InputEvent.CTRL_MASK));

miOpen.addActionListener(this);

miSave = new JMenuItem(new ImageIcon("images/save.gif"));

miSave.addActionListener(this);

miExit = new JMenuItem("Exit", KeyEvent.VK_X);

miExit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, 

InputEvent.CTRL_MASK));

miExit.addKeyListener(this);

miExit.addActionListener(this);

mFile.add(miNew);

mFile.add(miOpen);

mFile.add(miSave);

mFile.addSeparator();

mFile.add(miExit);

mHelp = new JMenu("Help");

this.mb = new JMenuBar();

this.mb.add(mFile);

this.mb.add(mHelp);

//this.mb.setHelpMenu(mHelp);

this.f.getRootPane().setJMenuBar(mb);

this.font = new Font("Monospaced", Font.PLAIN, 10);

this.toolbar = new JToolBar();

this.btnNew = new JButton(new ImageIcon("images/new.gif"));

this.btnNew.addActionListener(this);

this.btnOpen = new JButton(new ImageIcon("images/open.gif"));

this.btnOpen.addActionListener(this);

this.btnSave = new JButton(new ImageIcon("images/save.gif"));

this.btnSave.addActionListener(this);

this.ta = new JTextArea();

this.ta.setFont(font);

this.pane = new JScrollPane(this.ta, 

                    JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 

                    JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);

String [] array = {"2","5","8","10","12","14","18","20","25","30","35","40"};

this.combo = new JComboBox<String>(array);

this.combo.addItemListener(this);

this.combo1 = new JComboBox<String>();

this.combo1.addItemListener(this);

}

private void display(){

f.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

this.f.addWindowListener(new WindowAdapter(){

@Override

public void windowClosing(WindowEvent evt){

exitComm();

}

});

this.con.setLayout(new BorderLayout());

this.toolbar.add(this.btnNew);

this.toolbar.add(this.btnOpen);

this.toolbar.addSeparator();

this.toolbar.add(this.btnSave);

this.toolbar.addSeparator();

this.toolbar.add(this.combo);

this.combo.setSelectedItem(new String("10"));  //처음 기본으로 선택되어있는 사이즈가10

this.combo1.setModel(getModel());

this.combo1.setSelectedItem(new String("Monospaced")); //처음 기본으로 선택된폰트

this.toolbar.add(this.combo1);

this.con.add("North", this.toolbar);

this.con.add("Center", this.pane);

f.setSize(600,500);

f.setLocationRelativeTo(null);

f.setVisible(true);

}

private DefaultComboBoxModel getModel(){

Vector<String> vector = new Vector<String>(1,1);

GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();

String [] fontArray = env.getAvailableFontFamilyNames();

for(String str : fontArray)  vector.addElement(str);

return new DefaultComboBoxModel(vector);

}

@Override

public void keyReleased(KeyEvent evt){

switch(evt.getKeyCode()){

case KeyEvent.VK_X : exitComm(); break;

//case 'o' :

case KeyEvent.VK_S : saveFile();  break;

}

}

@Override

public void itemStateChanged(ItemEvent evt){

String str = (String)this.combo.getSelectedItem();

String fontname  = (String)this.combo1.getSelectedItem();

this.ta.setFont(new Font(fontname, Font.PLAIN, Integer.parseInt(str)));

}

@Override

public void actionPerformed(ActionEvent evt){

if(evt.getSource() == btnNew || evt.getSource() == miNew){

this.f.setTitle("Noname" + ++count +" - Notepad");

this.ta.setText("");

}else if(evt.getSource() == btnOpen || evt.getSource() == miOpen){

JFileChooser jc = new JFileChooser(".");

FileNameExtensionFilter filter = new FileNameExtensionFilter(

       "TXT & DAT Files", "txt", "dat");

jc.setFileFilter(filter);

int choice = jc.showOpenDialog(this.f);

if(choice == JFileChooser.APPROVE_OPTION){

File file = jc.getSelectedFile();

openFile(file);

}else return;

}else if(evt.getSource() == btnSave || evt.getSource() == miSave){

saveFile();

}else if(evt.getSource() == miExit){

exitComm();

}

}

private void exitComm(){  //종료버튼을 눌렀을때

if(ta.getText().length() > 0){ //한글자라도 입력되어있다면

int choice = JOptionPane.showConfirmDialog(f, "저장하시겠습니까?", 

                                     "종료", JOptionPane.YES_NO_CANCEL_OPTION,

                                     JOptionPane.QUESTION_MESSAGE);

switch(choice){

case JOptionPane.YES_OPTION : saveFile(); break;

case JOptionPane.NO_OPTION : System.exit(0); break;

case JOptionPane.CANCEL_OPTION :  break;

}

}else System.exit(0);  //한글자도 입력이 안되어있으면 그냥 종료

}

private void saveFile(){

JFileChooser jc = new JFileChooser(".");

FileNameExtensionFilter filter = new FileNameExtensionFilter(

       "TXT || DAT || Java Files", "txt", "dat", "java");

jc.setFileFilter(filter);

int choice = jc.showSaveDialog(this.f);

File file = null;

if(choice == JFileChooser.APPROVE_OPTION) 

file = jc.getSelectedFile();

else return;

BufferedWriter bw = null;

try {

bw = new BufferedWriter(new FileWriter(file));

bw.write(this.ta.getText());

this.f.setTitle(file.getAbsolutePath() + " - Notepad");

} catch (IOException e) {

JOptionPane.showMessageDialog(this.f, e.getMessage());

} finally{

try {

bw.close();

} catch (IOException e2) {}

}

}

private void openFile(File file){

BufferedReader br = null;

String line = null;

this.ta.setText("");

this.f.setTitle(file.getAbsolutePath() + " - Notepad");

try {

br = new BufferedReader(new FileReader(file));

while((line = br.readLine()) != null){

this.ta.append(line + "\n");

}

} catch (IOException e) {

JOptionPane.showMessageDialog(this.f, e.getMessage());

} finally{

try {

br.close();

} catch (IOException e2) {}

}

}

public static void main(String[] args) {

new Notepad().display();

}

}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

RandomAccess  - 한글 처리가 안된다.

import java.io.IOException;

import java.io.RandomAccessFile;


//한글 잘 될까요?

//length() - 사이즈, getFilePointer() - 현재포인터위치읽는, seek() - 원하는 위치에 포인터위치를 놓음

public class RandomAccessFileDemo {

public static void main(String[] args) {

String filePath = "src/RandomAccessFileDemo.java";

RandomAccessFile raf = null;

long size = 0;  //랜덤액새세는 처음나왔을때부터 사이즈가 long형

try {

raf = new RandomAccessFile(filePath, "r");

size = raf.length();

while(size > raf.getFilePointer()){ 

System.out.println(Utility.entoutf8(raf.readLine()));  //한 줄을 읽으면 포인터가 제일 마지막으로 이동, 한글이 나오게하기 위해 Utility클래스에서 따로 처리를 해줌.

}

} catch (IOException e) {

System.out.println(e.getMessage());

} finally{

try{

raf.close();

}catch(IOException ex){}

}

}

}

//랜덤액세스는 정말 쉽게 읽어들일 수 있으나, 한글이깨진다는 단점이 있다. 그래서 Utility class로 따로 처리를 해준다.


import java.io.UnsupportedEncodingException;


public class Utility {

public static String entoko(String en){

String ko = null;

try {

ko = new String(en.getBytes("ISO8859_1"), "KSC5601");

} catch (UnsupportedEncodingException e) {}

return ko;

}

public static String kotoen(String ko){

String en = null;

try {

en = new String(ko.getBytes("KSC5601"), "ISO8859_1");

} catch (UnsupportedEncodingException e) {}

return en;

}

public static String utf8toko(String utf8){

String ko = null;

try {

ko = new String(utf8.getBytes("UTF-8"), "KSC5601");

} catch (UnsupportedEncodingException e) {}

return ko;

}

public static String kotoutf8(String ko){

String utf8 = null;

try {

utf8 = new String(ko.getBytes("KSC5601"), "UTF-8");

} catch (UnsupportedEncodingException e) {}

return utf8;

}

public static String entoutf8(String en){

String utf8 = null;

try {

utf8 = new String(en.getBytes("ISO8859_1"), "UTF-8");

} catch (UnsupportedEncodingException e) {}

return utf8;

}

}

//new없이 접근하기 위해 모든 메소드를 static으로 만듬.

-----------------------------------------------------

Utility class는 위에 소스를 쓰고,

file로 demo.txt를 만들어서 아래와 같이 만들어줌.

89.5

true

5050

'A'

Hello

안녕하세요


import java.io.IOException;

import java.io.RandomAccessFile;



public class RandomAccessFileDemo1 {

public static void main(String[] args) throws IOException{

RandomAccessFile raf = null;

raf =new RandomAccessFile("src/demo.txt", "rw");

raf.seek(raf.length());    //파일 끝으로 이동

raf.writeUTF("\nWorld");  // '\n'을 넣고 글씨를 넣는 방법을 사용하면 안되는데, 이렇게 같이 넣어버리면 된다. 왜그런지는 잘몰라.

raf.writeUTF("\n한글연습");

raf.seek((long)0);     //파일 처음으로 이동

while(raf.length() > raf.getFilePointer()){

System.out.println(Utility.entoutf8(raf.readLine()));

}

raf.close();

}

}

출력:

89.5

true

5050

'A'

Hello

안녕하세요

World

한글연습

+ Recent posts