Skip to content

Commit 443c010

Browse files
committed
파일 첨부 기능 구현
1 parent f0d1c6b commit 443c010

32 files changed

+627
-86
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/node_modules
2+
/npm-debug.log
23
/public/storage
34
/vendor
45
/.idea
@@ -9,3 +10,4 @@
910
Homestead.json
1011
Homestead.yaml
1112
.env
13+
public/files

app/Article.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ public function user() {
3434
public function tags() {
3535
return $this->belongsToMany(Tag::class);
3636
}
37+
38+
public function attachments() {
39+
return $this->hasMany(Attachment::class);
40+
}
3741
}

app/Attachment.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace App;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class Attachment extends Model
8+
{
9+
/**
10+
* The attributes that are mass assignable.
11+
*
12+
* @var array
13+
*/
14+
protected $fillable = [
15+
'filename',
16+
'bytes',
17+
'mime',
18+
];
19+
20+
/**
21+
* The attributes that should be hidden for arrays.
22+
*
23+
* @var array
24+
*/
25+
protected $hidden = [
26+
'article_id',
27+
'created_at',
28+
'updated_at',
29+
];
30+
31+
/**
32+
* The accessors to append to the model's array form.
33+
*
34+
* @var array
35+
*/
36+
protected $appends = [
37+
'url',
38+
];
39+
40+
/* Relationships */
41+
42+
public function article()
43+
{
44+
return $this->belongsTo(Article::class);
45+
}
46+
47+
/* Accessors */
48+
49+
public function getBytesAttribute($value)
50+
{
51+
return format_filesize($value);
52+
}
53+
54+
public function getUrlAttribute()
55+
{
56+
return url('files/'.$this->filename);
57+
}
58+
}

app/Http/Controllers/ArticlesController.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function create()
5050
* @return \Illuminate\Http\Response
5151
*/
5252
public function store(\App\Http\Requests\ArticlesRequest $request) {
53+
// 글 저장
5354
$article = $request->user()->articles()->create($request->all());
5455

5556
if (! $article) {
@@ -58,8 +59,29 @@ public function store(\App\Http\Requests\ArticlesRequest $request) {
5859
return back()->withInput();
5960
}
6061

62+
// 태그 싱크
6163
$article->tags()->sync($request->input('tags'));
6264

65+
// if ($request->hasFile('files')) {
66+
// // 파일 저장
67+
// $files = $request->file('files');
68+
//
69+
// foreach($files as $file) {
70+
// $filename = str_random().filter_var($file->getClientOriginalName(), FILTER_SANITIZE_URL);
71+
//
72+
// // 순서 중요 !!!
73+
// // 파일이 PHP의 임시 저장소에 있을 때만 getSize, getClientMimeType등이 동작하므로,
74+
// // 우리 프로젝트의 파일 저장소로 업로드를 옮기기 전에 필요한 값을 취해야 함.
75+
// $article->attachments()->create([
76+
// 'filename' => $filename,
77+
// 'bytes' => $file->getSize(),
78+
// 'mime' => $file->getClientMimeType()
79+
// ]);
80+
//
81+
// $file->move(attachments_path(), $filename);
82+
// }
83+
// }
84+
6385
event(new \App\Events\ArticlesEvent($article));
6486
flash()->success('작성하신 글이 저장되었습니다.');
6587

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Http\Request;
6+
7+
use App\Http\Requests;
8+
9+
class AttachmentsController extends Controller
10+
{
11+
/**
12+
* AttachmentsController constructor.
13+
*/
14+
public function __construct()
15+
{
16+
$this->middleware('auth', ['except' => 'show']);
17+
}
18+
19+
/**
20+
* Store a newly created resource in storage.
21+
*
22+
* @param \Illuminate\Http\Request $request
23+
* @return \Illuminate\Http\Response
24+
*/
25+
public function store(Request $request)
26+
{
27+
$attachments = [];
28+
29+
if ($request->hasFile('files')) {
30+
$files = $request->file('files');
31+
32+
foreach($files as $file) {
33+
$filename = str_random().filter_var($file->getClientOriginalName(), FILTER_SANITIZE_URL);
34+
35+
$payload = [
36+
'filename' => $filename,
37+
'bytes' => $file->getClientSize(),
38+
'mime' => $file->getClientMimeType()
39+
];
40+
41+
$file->move(attachments_path(), $filename);
42+
43+
$attachments[] = ($id = $request->input('article_id'))
44+
? \App\Article::findOrFail($id)->attachments()->create($payload)
45+
: \App\Attachment::create($payload);
46+
}
47+
}
48+
49+
return response()->json($attachments, 200, [], JSON_PRETTY_PRINT);
50+
}
51+
52+
/**
53+
* Remove the specified resource from storage.
54+
*
55+
* @param \App\Attachment $attachment
56+
* @return \Illuminate\Http\Response
57+
*/
58+
public function destroy(\App\Attachment $attachment)
59+
{
60+
$path = attachments_path($attachment->name);
61+
62+
if (\File::exists($path)) {
63+
\File::delete($path);
64+
}
65+
66+
$attachment->delete();
67+
68+
return response()->json($attachment, 200, [], JSON_PRETTY_PRINT);
69+
}
70+
71+
/**
72+
* Display the specified resource.
73+
*
74+
* @param $file
75+
* @return \Illuminate\Contracts\Routing\ResponseFactory
76+
*/
77+
public function show($file)
78+
{
79+
$path = attachments_path($file);
80+
81+
if (! \File::exists($path)) {
82+
abort(404);
83+
}
84+
85+
$image = \Image::make($path);
86+
87+
return response($image->encode('png'), 200, [
88+
'Content-Type' => 'image/png'
89+
]);
90+
}
91+
}

app/Http/Requests/ArticlesRequest.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66

77
class ArticlesRequest extends FormRequest
88
{
9+
/**
10+
* The input keys that should not be flashed on redirect.
11+
*
12+
* @var array
13+
*/
14+
protected $dontFlash = [
15+
'files',
16+
];
17+
918
/**
1019
* Determine if the user is authorized to make this request.
1120
*
@@ -23,10 +32,14 @@ public function authorize()
2332
*/
2433
public function rules()
2534
{
35+
$mimes = implode(',', config('project.mimes'));
36+
2637
return [
2738
'title' => ['required'],
2839
'tags' => ['required', 'array'],
2940
'content' => ['required', 'min:10'],
41+
'files' => ['array'],
42+
'files.*' => ['sometimes', "mimes:{$mimes}", 'max:30000'],
3043
];
3144
}
3245

@@ -37,8 +50,11 @@ public function rules()
3750
*/
3851
public function messages() {
3952
return [
40-
'required' => ':attribute은(는) 필수 입력 항목입니다.',
41-
'min' => ':attribute은(는) 최소 :min 글자 이상이 필요합니다.',
53+
'required' => '필수 입력 항목입니다.',
54+
'min' => '최소 :min 글자 이상이 필요합니다.',
55+
'array' => '배열만 허용합니다.',
56+
'mimes' => ':values 형식만 허용합니다.',
57+
'max' => ':max 킬로바이트까지만 허용합니다.',
4258
];
4359
}
4460

@@ -52,6 +68,9 @@ public function attributes()
5268
return [
5369
'title' => '제목',
5470
'content' => '본문',
71+
'tags' => '태그',
72+
'files' => '파일',
73+
'files.*' => '파일',
5574
];
5675
}
5776
}

app/Providers/RouteServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public function boot()
2626
parent::boot();
2727

2828
Route::model('article', \App\Article::class);
29+
Route::model('attachment', \App\Attachment::class);
2930
}
3031

3132
/**

app/helpers.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,41 @@ function gravatar_url($email, $size = 48)
3737
{
3838
return sprintf("//www.gravatar.com/avatar/%s?s=%s", md5($email), $size);
3939
}
40-
}
40+
}
41+
42+
if (! function_exists('attachments_path')) {
43+
/**
44+
* Generate attachments path.
45+
*
46+
* @param string $path
47+
* @return string
48+
*/
49+
function attachments_path($path = null)
50+
{
51+
return public_path('files'.($path ? DIRECTORY_SEPARATOR.$path : $path));
52+
}
53+
}
54+
55+
if (! function_exists('format_filesize')) {
56+
/**
57+
* Calculate human-readable file size string.
58+
*
59+
* @param $bytes
60+
* @return string
61+
*/
62+
function format_filesize($bytes)
63+
{
64+
if (! is_numeric($bytes)) return 'NaN';
65+
66+
$decr = 1024;
67+
$step = 0;
68+
$suffix = ['bytes', 'KB', 'MB'];
69+
70+
while (($bytes / $decr) > 0.9) {
71+
$bytes = $bytes / $decr;
72+
$step ++;
73+
}
74+
75+
return round($bytes, 2) . $suffix[$step];
76+
}
77+
}

config/project.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,16 @@
2222
'server' => '서버',
2323
'tip' => '',
2424
],
25+
26+
/*
27+
|--------------------------------------------------------------------------
28+
| 업로드할 수 있는 파일 확장자
29+
|--------------------------------------------------------------------------
30+
*/
31+
'mimes' => [
32+
'png',
33+
'jpg',
34+
'zip',
35+
'tar',
36+
],
2537
];

database/factories/ModelFactory.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,12 @@
2525
'updated_at' => $date,
2626
];
2727
});
28+
29+
$factory->define(App\Attachment::class, function (Faker\Generator $faker) {
30+
return [
31+
'filename' => sprintf("%s.%s",
32+
str_random(),
33+
$faker->randomElement(config('project.mimes'))
34+
)
35+
];
36+
});

0 commit comments

Comments
 (0)